diff --git a/.circleci/config.yml b/.circleci/config.yml index b70d8ee382..ed9ee61a35 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,26 +1,75 @@ version: 2.1 orbs: - python: circleci/python@0.2.1 + python: circleci/python@4.0.0 jobs: build-and-test: - working_directory: ~/circleci-demo-python-django - docker: - - image: circleci/python:3.7.9 # primary container for the build job - auth: - username: mydockerhub-user - password: $DOCKERHUB_PASSWORD # context / project UI env-var reference + executor: + name: python/default + tag: '3.10' # or '3.12' + environment: + # HuggingFace: disable xet and use cache directory + # NOTE @deruyter92 2026-05-07: "xet" opens many simultaneous connections + # to different data chunks. Currently doesn't work well with CircleCI. + # See: https://github.com/huggingface/xet-core/issues/800 + HF_HUB_DISABLE_XET: 1 + HF_HOME: ~/.cache/huggingface + steps: - checkout - - python/load-cache - - python/install-deps - - python/save-cache + + # Restore uv cache + - restore_cache: + name: Restore uv cache + keys: + - v2-uv-pip-{{ checksum "pyproject.toml" }} + - v2-uv-pip- + + # Restore HuggingFace weights cache + - restore_cache: + name: Restore Hugging Face cache + keys: + - hf-weights-v1-{{ checksum "pyproject.toml" }} + - hf-weights-v1- + + # Install uv + - run: + name: Install uv + command: | + pip install uv + + # Install DeepLabCut runtime deps only + - run: + name: Install DeepLabCut runtime deps only + command: | + uv pip install --system -e . + + # (Optional) Trim the cache for CI so uploads stay small and fast + - run: + name: Prune uv cache for CI + command: uv cache prune --ci || true + + # Save the uv cache for next runs + - save_cache: + name: Save uv cache + key: v2-uv-pip-{{ checksum "pyproject.toml" }} + paths: + - ~/.cache/uv + + # Test DLC - run: - command: python testscript_cli.py name: TestDLC + command: python testscript_cli.py + + # Save the HF weights cache for next runs + - save_cache: + name: Save huggingface cache + key: hf-weights-v1-{{ checksum "pyproject.toml" }} + paths: + - ~/.cache/huggingface workflows: main: jobs: - - build-and-test \ No newline at end of file + - build-and-test diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000000..b46cf0e61f --- /dev/null +++ b/.codespellrc @@ -0,0 +1,5 @@ +[codespell] +skip = .git,*.pdf,*.svg,*.ipynb,deeplabcut/pose_estimation_tensorflow/models/pretrained +# MOT,SIE - legit acronyms +# tThe - for \tThe. codespell is not good detecting those yet +ignore-words-list = mot,sie,tthe,assertin,bu,td,ctd,wither diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index bf0e23d68a..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - -Thanks for opening this issue, and thanks for using DeepLabCut (we hope you are enjoying it ;). -Please fill out the template completely, including the full "TRACEBACK" and input code that you ran to hit this error. - -**Describe the bug** - -A clear and concise description of what the bug is. -Please provide the minimal required code to reproduce the error and the full output (please edit ONLY the section below that says `PLACE YOUR CODE HERE!!! `. - -**Desktop (please complete the following information about your system):** - - OS: [e.g. Windows10, MacOS version, Linux version, etc.] - - DeepLabCut Version [e.g. 22] (please check with ``import deeplabcut``, ``deeplabcut.__version__``) - - DeepLabCut mode, i.e. single animal tracking, multi-animal tracking, 3D tracking. - - Browser, if applicable [e.g. chrome, safari] - -**To Reproduce** -Steps to reproduce the behavior, i.e.: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. the input code you used (i.e. `deeplabcut.train_network( .... )`) -4. See error: - -
TRACEBACK

- -```python - -^do not delete the above "

TRACEBACK

" or the ```python part! -PLACE YOUR CODE HERE!!! -do not delete below this line, leave the blank line and the ``` - -``` -

- - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. (but please post the code output above, not images of code!) - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..764dfe563b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,97 @@ +name: Bug Report +description: File a bug report to help us improve +assignees: + - mmathislab #temp +body: + - type: markdown + attributes: + value: | + Thanks for opening this issue, and thanks for using DeepLabCut (we hope you are enjoying it ☺️) + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. Remove `is:open` from the search field to search closed (solved) issues. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Operating System + description: What operating system are you using? + placeholder: macOS Big Sur + validations: + required: true + - type: textarea + attributes: + label: DeepLabCut version + description: What version of DLC are you using? Please check with `import deeplabcut`, `deeplabcut.__version__` + placeholder: 3.0.0 + validations: + required: true + - type: dropdown + id: backend-engine + attributes: + label: What engine are you using? + options: + - pytorch + - tensorflow + - both (rare!) + validations: + required: true + - type: dropdown + id: dlcmode + attributes: + label: DeepLabCut mode + options: + - single animal + - multi animal + - 3d + validations: + required: true + - type: textarea + attributes: + label: Device type + description: What GPU/CPU are you using? + placeholder: GeForce 2080 RTX + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: Bug description 🐛 + description: Also tell us concisely what you expected to happen + placeholder: What happened? + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Please provide a minimal example to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images and other files by clicking this area to highlight it and then dragging files in. + - type: checkboxes + attributes: + label: Code of Conduct + description: The Code of Conduct helps create a safe space for everyone. We require that everyone agrees to it. + options: + - label: I agree to follow this project's [Code of Conduct](https://github.com/DeepLabCut/DeepLabCut/blob/master/CODE_OF_CONDUCT.md) + required: true + - type: markdown + attributes: + value: "Thanks for completing our bug report!" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..4dc993c9de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: +- name: deeplabcut forum + url: https://forum.image.sc/tag/deeplabcut + about: Please ask general questions here +- name: deeplabcut gitter + url: https://gitter.im/DeepLabCut/community + about: Chat with the community diff --git a/.github/ISSUE_TEMPLATE/problem-using-deeplabcut.md b/.github/ISSUE_TEMPLATE/problem-using-deeplabcut.md deleted file mode 100644 index 18629fe33f..0000000000 --- a/.github/ISSUE_TEMPLATE/problem-using-deeplabcut.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: Problem using DeepLabCut -about: Describe what the problem is - ---- - -Thanks for opening this issue, and thanks for using DeepLabCut (we hope you are enjoying it ;). Please fill out the template completely, including the full "traceback" and input code that you ran to hit this error. - -**Your Operating system and DeepLabCut version** - -Please state your operating system, env, and which version of DeepLabCut you are using. -Example: Ubuntu 16.04 LTS, with an Anaconda Env, & DeepLabCut1.x or 2.x. - -Please complete the following information about your system: - -OS: [e.g. MacOS, Windows 10, etc] -DeepLabCut Version: [e.g. 2.2] (please check with import deeplabcut, deeplabcut.__version__) -Anaconda env used: - -**Describe the problem** - -A clear and concise description of what the problem is. - -Please place code inside this: - -
Code output

- -[Please copy/paste the full terminal output of the error here!!!] - -

- -**How to Reproduce the problem** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - - -**Screenshots** -If applicable, add screenshots to help explain your problem. - - -**Additional context** -Add any other context about the problem here. diff --git a/.github/actions/setup-dev-docs/action.yaml b/.github/actions/setup-dev-docs/action.yaml new file mode 100644 index 0000000000..bbcbdc0d5d --- /dev/null +++ b/.github/actions/setup-dev-docs/action.yaml @@ -0,0 +1,35 @@ +name: Setup dev-docs environment +description: Install Python, system deps, and DeepLabCut dev-docs dependencies. + +inputs: + python-version: + description: Python version to use. + required: false + default: "3.10" + + install-system-deps: + description: Install system dependencies required by mkdocs-material social plugin. + required: false + default: "true" + +runs: + using: composite + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install system dependencies for mkdocs-material social plugin + if: ${{ inputs.install-system-deps == 'true' }} + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev \ + libjpeg-dev libpng-dev libz-dev + + - name: Install dev-docs dependencies + shell: bash + run: | + python -m pip install --upgrade pip + python -m pip install ".[dev-docs]" diff --git a/.github/workflows/build-dev-docs.yml b/.github/workflows/build-dev-docs.yml new file mode 100644 index 0000000000..125c68fc2b --- /dev/null +++ b/.github/workflows/build-dev-docs.yml @@ -0,0 +1,33 @@ +name: Docs / Build dev-docs + +on: + workflow_call: + inputs: + python-version: + description: Python version used to build the dev docs. + required: false + default: "3.10" + type: string + + config-file: + description: MkDocs config file. + required: false + default: "dev-docs/mkdocs.yml" + type: string + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v6 + + - name: Set up dev-docs environment + uses: ./.github/actions/setup-dev-docs + with: + python-version: ${{ inputs.python-version }} + + - name: Build dev-docs + run: mkdocs build -v -f ${{ inputs.config-file }} diff --git a/.github/workflows/build-main-docs.yml b/.github/workflows/build-main-docs.yml new file mode 100644 index 0000000000..e74ee9a0d6 --- /dev/null +++ b/.github/workflows/build-main-docs.yml @@ -0,0 +1,49 @@ +name: Docs / Build main docs (Jupyter Book) + +on: + workflow_call: + inputs: + python-version: + description: "Python version used to build the docs." + required: false + default: "3.10" + type: string + build_dir: + required: false + default: "./_build/html" + type: string + upload_artifact: + description: "If true, upload the built site as an artifact." + required: false + default: false + type: boolean + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install docs dependencies + run: | + python -m pip install --upgrade pip + python -m pip install .[docs] + + - name: Build the book + run: jupyter-book build . + + - name: Upload built site artifact + if: ${{ inputs.upload_artifact }} + uses: actions/upload-artifact@v6 + with: + name: built-book + path: ${{ inputs.build_dir }} + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000000..cac97bac77 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,22 @@ +--- +name: Code / Codespell + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + branches: [ main ] + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Annotate locations with typos + uses: codespell-project/codespell-problem-matcher@v1 + - name: Codespell + uses: codespell-project/actions-codespell@v2 diff --git a/.github/workflows/deploy-dev-docs-mike.yml b/.github/workflows/deploy-dev-docs-mike.yml new file mode 100644 index 0000000000..7a396c38d9 --- /dev/null +++ b/.github/workflows/deploy-dev-docs-mike.yml @@ -0,0 +1,129 @@ +name: Docs / Deploy dev-docs with Mike + +on: + workflow_call: + inputs: + action: + description: "Mike action to perform: deploy or delete." + required: true + type: string + + version_label: + description: "Version label to deploy/delete, e.g. main or 3.0." + required: true + type: string + + aliases: + description: "Space-separated aliases to assign on deploy, e.g. latest-release." + required: false + default: "" + type: string + + default_label: + description: "If non-empty, run mike set-default with this label or alias after deploy." + required: false + default: "" + type: string + + git_tag: + description: "Optional git tag whose deeplabcut package should be checked out before deploying." + required: false + default: "" + type: string + + python-version: + description: Python version used for dev docs. + required: false + default: "3.10" + type: string + + config-file: + description: MkDocs config file. + required: false + default: "dev-docs/mkdocs.yml" + type: string + + deploy-prefix: + description: Mike deploy prefix. + required: false + default: "dev" + type: string + +jobs: + mike: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Validate inputs + shell: bash + run: | + case "${{ inputs.action }}" in + deploy|delete) + ;; + *) + echo "::error::Unsupported action '${{ inputs.action }}'. Expected 'deploy' or 'delete'." + exit 1 + ;; + esac + + if [ "${{ inputs.action }}" = "deploy" ] && [ -z "${{ inputs.git_tag }}" ] && [ "${{ inputs.version_label }}" != "main" ]; then + echo "::error::git_tag is required when deploying non-'main' versions." + exit 1 + fi + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check out tagged package source + if: inputs.git_tag != '' + shell: bash + run: | + git checkout "${{ inputs.git_tag }}" -- deeplabcut + + - name: Set up dev-docs environment + uses: ./.github/actions/setup-dev-docs + with: + python-version: ${{ inputs.python-version }} + install-system-deps: ${{ inputs.action != 'delete' }} + + - name: Configure git for Mike + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy dev-docs version + if: inputs.action == 'deploy' + shell: bash + run: | + if [ -n "${{ inputs.aliases }}" ]; then + mike deploy --push \ + --config-file "${{ inputs.config-file }}" \ + --deploy-prefix "${{ inputs.deploy-prefix }}" \ + --update-aliases \ + "${{ inputs.version_label }}" ${{ inputs.aliases }} + else + mike deploy --push \ + --config-file "${{ inputs.config-file }}" \ + --deploy-prefix "${{ inputs.deploy-prefix }}" \ + "${{ inputs.version_label }}" + fi + + if [ -n "${{ inputs.default_label }}" ]; then + mike set-default --push \ + --config-file "${{ inputs.config-file }}" \ + --deploy-prefix "${{ inputs.deploy-prefix }}" \ + "${{ inputs.default_label }}" + fi + + - name: Delete dev-docs version + if: inputs.action == 'delete' + shell: bash + run: | + mike delete --push \ + --config-file "${{ inputs.config-file }}" \ + --deploy-prefix "${{ inputs.deploy-prefix }}" \ + "${{ inputs.version_label }}" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..96f78318bb --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,54 @@ +name: Docs / Deploy docs (main + dev-docs main) + +on: + push: + branches: [ main ] + +permissions: + contents: write + +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +jobs: + build-main-docs: + uses: ./.github/workflows/build-main-docs.yml + with: + python-version: "3.10" + build_dir: "./_build/html" + upload_artifact: true + secrets: inherit + + deploy-main-docs: + needs: build-main-docs + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download built Jupyter Book artifact + uses: actions/download-artifact@v4 + with: + name: built-book + path: site + + - name: Deploy main docs to gh-pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site + keep_files: true + + deploy-dev-docs-main: + needs: deploy-main-docs + uses: ./.github/workflows/deploy-dev-docs-mike.yml + with: + action: deploy + version_label: main + aliases: "" + default_label: "" + python-version: "3.10" + config-file: dev-docs/mkdocs.yml + deploy-prefix: dev + secrets: inherit diff --git a/.github/workflows/docs_and_notebooks_checks.yml b/.github/workflows/docs_and_notebooks_checks.yml new file mode 100644 index 0000000000..8248f7a53e --- /dev/null +++ b/.github/workflows/docs_and_notebooks_checks.yml @@ -0,0 +1,105 @@ +name: Docs / Docs & notebooks freshness and formatting checks + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + staleness: + name: Docs and notebooks scan (changed docs only) + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repository (full history for git dates) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install staleness tool dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "pydantic>=2,<3" pyyaml "nbformat>=5" + + - name: Collect changed .md/.ipynb files + id: changed_docs + shell: bash + run: | + set -euo pipefail + mkdir -p tmp/docs_nb_checks + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + base="${{ github.event.pull_request.base.sha }}" + head="${{ github.event.pull_request.head.sha }}" + else + base="${{ github.event.before }}" + head="${{ github.sha }}" + fi + + git diff --name-only --diff-filter=ACMR "$base" "$head" \ + | { grep -iE '\.(md|ipynb)$' || true; } \ + | { grep -E '^(docs/|tools/|examples/COLAB/|examples/JUPYTER/)' || true; } \ + | sort -u > tmp/docs_nb_checks/changed_docs.txt + + count=$(wc -l < tmp/docs_nb_checks/changed_docs.txt | tr -d ' ') + echo "count=$count" >> "$GITHUB_OUTPUT" + + echo "Changed docs files:" + if [[ "$count" -eq 0 ]]; then + echo "(none)" + else + sed 's/^/- /' tmp/docs_nb_checks/changed_docs.txt + fi + + - name: Run staleness report (read-only) + if: steps.changed_docs.outputs.count != '0' + shell: bash + run: | + set -euo pipefail + mapfile -t targets < tmp/docs_nb_checks/changed_docs.txt + + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + report \ + --targets "${targets[@]}" + + - name: Run staleness policy check (optional gate) + if: steps.changed_docs.outputs.count != '0' + continue-on-error: true + shell: bash + run: | + set -euo pipefail + mapfile -t targets < tmp/docs_nb_checks/changed_docs.txt + + python tools/docs_and_notebooks_check.py \ + --config tools/docs_and_notebooks_report_config.yml \ + --out-dir tmp/docs_nb_checks \ + --no-step-summary \ + check \ + --targets "${targets[@]}" + + - name: No changed docs to scan + if: steps.changed_docs.outputs.count == '0' + run: echo "No changed .md or .ipynb files found in the repo. Skipping scan." + + - name: Upload staleness artifacts + if: steps.changed_docs.outputs.count != '0' + uses: actions/upload-artifact@v4 + with: + name: staleness-report + path: | + tmp/docs_nb_checks/*.json + tmp/docs_nb_checks/*.md + tmp/docs_nb_checks/changed_docs.txt + if-no-files-found: error diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..3e32f6c5da --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,119 @@ +name: Code / pre-commit (PR only on changed files) + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + detect_changes: + 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@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - 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: 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 + if: ${{ needs.detect_changes.outputs.changed != '' }} + + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - 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/.github/workflows/intelligent-testing.yml b/.github/workflows/intelligent-testing.yml new file mode 100644 index 0000000000..a203558542 --- /dev/null +++ b/.github/workflows/intelligent-testing.yml @@ -0,0 +1,176 @@ +name: Code / Intelligent Python Testing + +on: + push: + branches: [ main ] + pull_request: + types: [opened, synchronize, reopened] + branches: [ main ] + +concurrency: + group: intelligent-${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || format('run-{0}', github.run_id) }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + intelligent-test-selection: + name: Select test plan + runs-on: ubuntu-latest + outputs: + run_skip: ${{ steps.selector.outputs.run_skip }} + run_docs: ${{ steps.selector.outputs.run_docs }} + run_fast: ${{ steps.selector.outputs.run_fast }} + run_full: ${{ steps.selector.outputs.run_full }} + selected_workflows: ${{ steps.selector.outputs.selected_workflows }} + lane_reasons: ${{ steps.selector.outputs.lane_reasons }} + pytest_paths: ${{ steps.selector.outputs.pytest_paths }} + functional_scripts: ${{ steps.selector.outputs.functional_scripts }} + reasons: ${{ steps.selector.outputs.reasons }} + changed_files: ${{ steps.selector.outputs.changed_files }} + provenance: ${{ steps.selector.outputs.provenance }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 # needed for merge-base/diff to be reliable + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Run selector + generate report + id: selector + run: | + python -m pip install --upgrade pip + pip install "pydantic>=2,<3" + python tools/test_selector.py \ + --write-github-output \ + --write-summary \ + --report-dir tmp/test-selection \ + --json + + - name: Upload selector report + uses: actions/upload-artifact@v6 + with: + name: test-selection-report + path: | + tmp/test-selection/selection.json + tmp/test-selection/decision.md + retention-days: 7 + if-no-files-found: error + + + + skip: + name: Skipped (lint config change) + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_skip == 'true' + runs-on: ubuntu-latest + steps: + - run: | + echo "Only lint config files changed; skipping docs/tests." + echo "Pre-commit workflow is responsible for lint config validation." + + + docs: + name: Main docs build + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_docs == 'true' + uses: ./.github/workflows/build-main-docs.yml + with: + python-version: "3.10" + build_dir: "./_build/html" + upload_artifact: false + secrets: inherit + + dev-docs: + name: Dev docs build + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_docs == 'true' + uses: ./.github/workflows/build-dev-docs.yml + with: + python-version: "3.10" + secrets: inherit + + + fast-tests: + name: Fast lane (targeted pytest + selected functional) + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_fast == 'true' + uses: ./.github/workflows/python-package.yml + with: + concurrency_key: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || '' }} + matrix_json: >- + {"include":[{"os":"ubuntu-latest","python-version":"3.12", "extras": "[tf]"}]} + pytest_paths_json: ${{ needs.intelligent-test-selection.outputs.pytest_paths }} + functional_scripts_json: ${{ needs.intelligent-test-selection.outputs.functional_scripts }} + full_suite: false + + full-tests: + name: Full matrix tests + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_full == 'true' + uses: ./.github/workflows/python-package.yml + with: + concurrency_key: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || '' }} + matrix_json: >- + { + "include": [ + {"os":"ubuntu-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"ubuntu-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"ubuntu-latest","python-version":"3.12", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"macos-latest","python-version":"3.12", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.10", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.11", "extras": "[tf]"}, + {"os":"windows-latest","python-version":"3.12", "extras": "[tf]"} + ] + } + full_suite: true + + tf-install-smoke-test: + name: TensorFlow install smoke test + needs: intelligent-test-selection + if: needs.intelligent-test-selection.outputs.run_full == 'true' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.10', '3.11', '3.12'] + # Run smoke test on the extras that are not tested in the full matrix tests + extras: ["[tf-cu11]", "[tf-cu12]"] + exclude: + - os: windows-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.12' + - extras: "[tf-cu11]" + python-version: '3.12' + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python + uses: conda-incubator/setup-miniconda@v3 + with: + channels: conda-forge + channel-priority: strict + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + shell: bash -el {0} + run: | + python -m ensurepip --upgrade + python -m pip install --upgrade pip setuptools wheel + python -m pip install dependency-groups + python -m pip install --no-cache-dir -e ".${{ matrix.extras }}" --group dev + + - name: Run TensorFlow install smoke test + shell: bash -el {0} + run: | + pytest tests/test_tf_install_smoke.py diff --git a/.github/workflows/lockfile-check.yml b/.github/workflows/lockfile-check.yml new file mode 100644 index 0000000000..f612f54668 --- /dev/null +++ b/.github/workflows/lockfile-check.yml @@ -0,0 +1,37 @@ +name: Code / Lockfile + +on: + pull_request: + paths: + - "pyproject.toml" + - "uv.lock" + # - ".python-version" + - ".github/workflows/uv-lockfile-check.yml" + push: + branches: [main] + paths: + - "pyproject.toml" + - "uv.lock" + # - ".python-version" + - ".github/workflows/uv-lockfile-check.yml" + +jobs: + uv-lockfile-check: + name: uv.lock is current + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: pyproject.toml + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.10.10" + + - name: Check lockfile + run: uv lock --check diff --git a/.github/workflows/manage-dev-docs.yml b/.github/workflows/manage-dev-docs.yml new file mode 100644 index 0000000000..fada72d0df --- /dev/null +++ b/.github/workflows/manage-dev-docs.yml @@ -0,0 +1,61 @@ +name: Docs / Manage dev-docs versions + +on: + workflow_dispatch: + inputs: + action: + description: "Action to perform" + required: true + type: choice + options: + - deploy-version + - delete-version + + version_label: + description: "Version label to deploy or delete, e.g. 3.0" + required: true + type: string + + git_tag: + description: "Git tag to check out for deploy-version, e.g. v3.0.0rc14. Ignored for delete-version." + required: false + type: string + + mark_latest_release: + description: "Alias this version as latest-release and set /dev/ default to latest-release." + required: false + type: boolean + default: false + +permissions: + contents: write + +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +jobs: + deploy-version: + if: inputs.action == 'deploy-version' + uses: ./.github/workflows/deploy-dev-docs-mike.yml + with: + action: deploy + version_label: ${{ inputs.version_label }} + git_tag: ${{ inputs.git_tag }} + aliases: ${{ inputs.mark_latest_release && 'latest-release' || '' }} + default_label: ${{ inputs.mark_latest_release && 'latest-release' || '' }} + python-version: "3.10" + config-file: dev-docs/mkdocs.yml + deploy-prefix: dev + secrets: inherit + + delete-version: + if: inputs.action == 'delete-version' + uses: ./.github/workflows/deploy-dev-docs-mike.yml + with: + action: delete + version_label: ${{ inputs.version_label }} + python-version: "3.10" + config-file: dev-docs/mkdocs.yml + deploy-prefix: dev + secrets: inherit diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9b0338a364..db2a30a1f4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,75 +1,250 @@ name: Python package on: - push: - branches: [ master ] - pull_request: - branches: [ master ] + workflow_call: + # This workflow can be called by other workflows (like intelligent-testing.yml) + inputs: + concurrency_key: + required: false + type: string + matrix_json: + required: true + type: string + pytest_paths_json: + required: false + type: string + default: "[]" + functional_scripts_json: + required: false + type: string + default: "[]" + full_suite: + required: false + type: boolean + default: false + + workflow_dispatch: + inputs: + concurrency_key: + description: "Optional stable key to dedupe manual runs (for example: pr-123)" + required: false + type: string + matrix_json: + description: "JSON matrix definition" + required: true + type: string + default: '{"include":[{"os":"ubuntu-latest","python-version":"3.12","extras": ""}]}' + pytest_paths_json: + description: "JSON array of pytest paths" + required: false + type: string + default: "[]" + functional_scripts_json: + description: "JSON array of functional scripts" + required: false + type: string + default: "[]" + full_suite: + description: "Run full pytest and default functional suite" + required: false + type: boolean + default: false jobs: build: runs-on: ${{ matrix.os }} + # Cancel outdated runs on the same OS and Python version when new commits are pushed + # Only cancels on PRs. + # Use a stable concurrency key only when one is explicitly provided + # (e.g. PR/workflow_call/manual dedupe). Otherwise fall back to github.run_id + # so pushes to main never cancel each other. + concurrency: + group: >- + tests-${{ github.workflow }}- + ${{ github.event_name == 'workflow_call' && inputs.concurrency_key + || github.event_name == 'workflow_dispatch' && inputs.concurrency_key + || github.run_id }}- + ${{ matrix.os }}-${{ matrix.python-version }} + cancel-in-progress: true strategy: fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8] #3.9 fails due to opencv-headless version - include: - - os: ubuntu-latest - path: ~/.cache/pip - - os: macos-latest - path: ~/Library/Caches/pip - - os: windows-latest - path: ~\AppData\Local\pip\Cache - + matrix: ${{ fromJson(inputs.matrix_json) }} + steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ matrix.path }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + - name: Free up disk space + if: runner.os == 'Linux' + run: | + echo "Disk space before cleanup:" + df -h + # Remove unnecessary software to free up disk space + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + echo "Disk space after cleanup:" + df -h - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Set up Python + uses: conda-incubator/setup-miniconda@v3 with: + channels: conda-forge + channel-priority: strict python-version: ${{ matrix.python-version }} - name: Install dependencies + shell: bash -el {0} # Important to enable conda env run: | + python -m ensurepip --upgrade python -m pip install --upgrade pip setuptools wheel - pip install -r requirements.txt - - - name: Install tensorflow - if: matrix.python-version < 3.8 - run: pip install tensorflow==1.15.5 - - - name: Install ffmpeg - if: matrix.python-version < 3.8 + python -m pip install dependency-groups + python -m pip install --no-cache-dir -e ".${{ matrix.extras }}" --group dev + + - name: Install ffmpeg (Linux/macOS) + if: runner.os != 'Windows' + shell: bash run: | if [ "$RUNNER_OS" == "Linux" ]; then - sudo apt-get install ffmpeg + sudo apt-get update + sudo apt-get install -y ffmpeg elif [ "$RUNNER_OS" == "macOS" ]; then - brew install ffmpeg - else - choco install ffmpeg + brew install ffmpeg || true fi - shell: bash + - name: Install ffmpeg (Windows, pinned monthly BtbN build) + # NOTE: The pinned version should be retained for ~2 years. This WILL fail if the BtbN release is removed, + # so if you are two years in the future and this step fails, please check the builds. Thanks. + if: runner.os == 'Windows' + shell: pwsh + env: + FFMPEG_TAG: autobuild-2026-03-31-13-11 + FFMPEG_ASSET: ffmpeg-N-123777-g53537f6cf5-win64-gpl-shared.zip + run: | + $ErrorActionPreference = "Stop" + + $tag = $env:FFMPEG_TAG + $asset = $env:FFMPEG_ASSET + + $baseUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/$tag" + $url = "$baseUrl/$asset" + $checksumsUrl = "$baseUrl/checksums.sha256" + + $tmpRoot = Join-Path $env:RUNNER_TEMP "ffmpeg-install" + $zip = Join-Path $tmpRoot $asset + $checksums = Join-Path $tmpRoot "checksums.sha256" + $dest = Join-Path $tmpRoot "ffmpeg" + + if (Test-Path -LiteralPath $tmpRoot) { + Remove-Item -LiteralPath $tmpRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $tmpRoot | Out-Null + + Invoke-WebRequest -Uri $url -OutFile $zip + Invoke-WebRequest -Uri $checksumsUrl -OutFile $checksums - - name: Run pytest tests - if: matrix.python-version < 3.8 + $expected = Get-Content -LiteralPath $checksums | + ForEach-Object { + if ($_ -match '^(?[0-9A-Fa-f]{64})\s+\*?(?.+)$' -and $matches.name.Trim() -eq $asset) { + $matches.sha.ToLowerInvariant() + } + } | + Select-Object -First 1 + + if (-not $expected) { + throw "Could not find checksum for $asset in $checksums" + } + + $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + throw "FFmpeg checksum mismatch. Expected $expected but got $actual" + } + + Expand-Archive -LiteralPath $zip -DestinationPath $dest -Force + $ffdir = Get-ChildItem -LiteralPath $dest -Directory | Select-Object -First 1 + if (-not $ffdir) { + throw "Could not find extracted FFmpeg directory." + } + + $binDir = Join-Path $ffdir.FullName "bin" + $binDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Verify ffmpeg/ffprobe available + shell: bash -el {0} run: | - pip install pytest - python -m pytest + set -e + ffmpeg -version + ffprobe -version - - name: Run functional tests - if: matrix.python-version < 3.8 + - name: Run pytest + shell: bash -el {0} + env: + FULL_SUITE: ${{ inputs.full_suite }} + PYTEST_PATHS_JSON: ${{ inputs.pytest_paths_json }} run: | - pip install git+git://github.com/${{ github.repository }}.git@${{ github.sha }} - python examples/testscript.py - python examples/testscript_multianimal.py + python - << 'PY' + import json + import os + import subprocess + import sys + + full_suite = os.environ["FULL_SUITE"].lower() == "true" + + if full_suite: + cmd = [sys.executable, "-m", "pytest"] + print("Running full pytest suite") + else: + paths = json.loads(os.environ.get("PYTEST_PATHS_JSON", "[]")) + if not paths: + print("No pytest paths selected; skipping pytest.") + raise SystemExit(0) + cmd = [sys.executable, "-m", "pytest", *paths] + print("Running targeted pytest:", " ".join(paths)) + + raise SystemExit(subprocess.call(cmd)) + PY + + - name: Run functional scripts + shell: bash -el {0} + env: + FULL_SUITE: ${{ inputs.full_suite }} + FUNCTIONAL_SCRIPTS_JSON: ${{ inputs.functional_scripts_json }} + run: | + python - << 'PY' + import json + import os + import subprocess + import sys + + def is_windows_python_3_11_or_greater() -> bool: + """Aligned with pyproject: .[tf*] omits TensorFlow on Windows for Python 3.11+.""" + return sys.platform == "win32" and sys.version_info >= (3, 11) + + full_suite = os.environ["FULL_SUITE"].lower() == "true" + if full_suite: + scripts = [ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ] + else: + scripts = json.loads(os.environ.get("FUNCTIONAL_SCRIPTS_JSON", "[]")) + if not scripts: + print("No functional scripts selected; skipping functional tests.") + raise SystemExit(0) + + for script in scripts: + if "tensorflow" in script and is_windows_python_3_11_or_greater(): + ver = f"{sys.version_info.major}.{sys.version_info.minor}" + print( + f"Skipping TensorFlow example on Windows {ver} (no TF in .[tf*] for 3.11+): {script}" + ) + continue + print("Running:", script) + rc = subprocess.call([sys.executable, script]) + if rc != 0: + raise SystemExit(rc) + PY diff --git a/.gitignore b/.gitignore index ff64e1eea8..7656027ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,30 @@ +# Docker specific +logs/ +#Jupyter book build directory +_build/* #Data and examples /examples/open* /examples/Reac* -/examples/TES* -/examples/multi* -/examples/3D* +/examples/m3* /examples/OUT +/examples/pretrained* .local - +.DS_Store +examples/.DS_Store +*~ # Tensorflow checkpoints *.ckpt snapshot-* +# Modelzoo checkpoints +deeplabcut/modelzoo/checkpoints/ + +# PyTorch backbone weights +deeplabcut/pose_estimation_pytorch/models/backbones/pretrained_weights/ + +# Wandb files +wandb/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -109,6 +123,10 @@ ENV/ .spyderproject .spyproject +# IDEs configurations +.vscode/* +.idea/* + # Rope project settings .ropeproject @@ -117,3 +135,16 @@ ENV/ # mypy .mypy_cache/ + +# Tools output +tmp/* + +# Test data +tests/data/* + +# Automated docs checks +**/tmp/docs_nb_checks/ + + +# Automatic test selection report +**/tmp/test-selection/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..21f085c90f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,128 @@ +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 + stages: [pre-commit, manual] + - id: check-merge-conflict + stages: [pre-commit, manual] + - id: name-tests-test + args: [--pytest-test-first] + stages: [pre-commit, manual] + - id: check-json + stages: [pre-commit, manual] + + # These modify files. Run locally only (pre-commit stage). + - id: end-of-file-fixer + stages: [pre-commit] + - id: trailing-whitespace + stages: [pre-commit] + + - repo: https://github.com/tox-dev/pyproject-fmt + 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] + + # 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] + + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + # -------------------------- + # LOCAL AUTOFIX (developers) + # -------------------------- + - id: ruff-check + name: ruff-check (fix) + args: [--fix, --unsafe-fixes] + stages: [pre-commit] + + - id: ruff-format + name: ruff-format (write) + stages: [pre-commit] + + # -------------------------- + # CI CHECK-ONLY (no writes) + # -------------------------- + - id: ruff-check + name: ruff-check (ci) + args: [--output-format=github] + stages: [manual] + + - id: ruff-format + name: ruff-format (ci) + args: [--check, --diff] + stages: [manual] + + # docs/ and dev-docs/ use different MkDocs/Markdown dialects, so each tree gets its + # own mdformat hook with the matching plugin (see https://github.com/hukkin/mdformat/issues/546). + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 + hooks: + - id: mdformat + name: mdformat (docs — myst) + additional_dependencies: + - mdformat-myst + files: ^docs/.*\.md$ + stages: [pre-commit] + + - id: mdformat + name: mdformat (dev-docs — mkdocs) + additional_dependencies: + - mdformat-mkdocs==5.2.1 + # Autoref targets may not resolve in the formatter env; without this flag, + # mdformat-mkdocs can rewrite links incorrectly (see KyleKing/mdformat-mkdocs#80). + args: [--ignore-missing-references] + files: ^dev-docs/.*\.md$ + stages: [pre-commit] + + # check only, no modifications + - repo: local + hooks: + - id: dlc-docs-notebooks-check + name: DLC docs+notebooks staleness/check + nbformat validate + normalization + entry: python tools/docs_and_notebooks_check.py + language: python + pass_filenames: true + files: ^(docs/|examples/(JUPYTER|COLAB)/|tools/).*(\.md|\.ipynb)$ + exclude: ^tools/docs_audits/ + args: + - --config + - tools/docs_and_notebooks_report_config.yml + - check + - --targets + additional_dependencies: + - "pydantic>=2,<3" + - "pyyaml" + - "nbformat>=5" + stages: [pre-commit, manual] diff --git a/AUTHORS b/AUTHORS index 341154d872..d53068678f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,20 +1,20 @@ DeepLabCut (www.deeplabcut.org) was initially developed by -Alexander & Mackenzie Mathis in collaboration with Matthias Bethge. +Alexander & Mackenzie Mathis in collaboration with Matthias Bethge in 2017. +It is actively developed by Alexander & Mackenzie Mathis (steering council and owners). DeepLabCut is an open-source tool and has benefited from suggestions and edits by many -individuals: -https://github.com/AlexEMG/DeepLabCut/graphs/contributors +individuals: DeepLabCut/graphs/contributors ############################################################################################################ DeepLabCut 1.0 Toolbox -A Mathis, alexander.mathis@bethgelab.org | https://github.com/AlexEMG/DeepLabCut +A Mathis, alexander.mathis@bethgelab.org | https://github.com/DeepLabCut/DeepLabCut M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab Specific external contributors: E Insafutdinov and co-authors of DeeperCut (see README) for feature detectors: https://github.com/eldar -- Thus, code in this subdirectory https://github.com/AlexEMG/DeepLabCut/tree/master/deeplabcut/pose_estimation_tensorflow -was adapted from: https://github.com/eldar/pose-tensorflow +- Thus, code in this subdirectory at the time of April 2018, deeplabcut/pose_estimation_tensorflow +was adapted from: https://github.com/eldar/pose-tensorflow. Products: DeepLabCut: markerless pose estimation of user-defined body parts with deep learning. Nature Neuroscience, 2018. @@ -33,7 +33,7 @@ These authors jointly directed this work: M. Mathis, M. Bethge ############################################################################################################ DeepLabCut 2.0 Toolbox -A Mathis, alexander.mathis@bethgelab.org | https://github.com/AlexEMG/DeepLabCut +A Mathis, alexander.mathis@bethgelab.org | https://github.com/DeepLabCut/DeepLabCut T Nath, nath@rowland.harvard.edu | https://github.com/meet10may M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab @@ -53,7 +53,7 @@ Writing: MWM, AM and TN with inputs from all authors. ############################################################################################################ DeepLabCut 2.1 major additions: -A Mathis, alexander.mathis@bethgelab.org | https://github.com/AlexEMG/DeepLabCut +A Mathis, alexander.mathis@bethgelab.org | https://github.com/DeepLabCut/DeepLabCut T Nath, nath@rowland.harvard.edu | https://github.com/meet10may M Yüksekgönül, mertyuksekgonul@gmail.com | https://github.com/mertyg M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab @@ -61,18 +61,20 @@ M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab Specific external contributors: Tensorpack augmentation: https://github.com/DeepLabCut/DeepLabCut/pull/409 by Katie Rupp -Preprint: -Pretraining boosts out-of-domain robustness for pose estimation -A. Mathis, M. Yüksekgönül, B. Rogers, M. Bethge, M. Mathis +Products: +Pretraining boosts out-of-domain robustness for pose estimation. WACV, 2021. +http://www.mackenziemathislab.org/horse10 +A. Mathis, T. Biasi, S. Schneider, M. Yüksekgönül, B. Rogers, M. Bethge, M. Mathis ############################################################################################################ DeepLabCut 2.1 - 2.2 additions: -A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG/DeepLabCut +A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG J Lauer, jessy@deeplabcut.org | https://github.com/jeylau M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab M Zhou, https://github.com/zhoumu53 S Ye, https://github.com/yeshaokai +S Schneider, https://github.com/stes T Biasi, https://github.com/tbiasi G Kane, https://github.com/gkane26 M Yüksekgönül, https://github.com/mertyg @@ -80,6 +82,58 @@ T Nath, https://github.com/meet10may Preprint: Multi-animal pose estimation and tracking with DeepLabCut -J Lauer, M Zhou, S Ye, W Menegas, T Nath, MM Rahman, V Di Santo, -D Soberanes, G Feng, VN Murthy, G Lauder, C Dulac, M Mathis, A Mathis +J Lauer, M Zhou, S Ye, W Menegas, S Schneider, T Nath, MM Rahman, V Di Santo, +D Soberanes, G Feng, VN Murthy, G Lauder, C Dulac, M Mathis, A Mathis (2021). https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1 + +Publication: +Multi-animal pose estimation, identification and tracking with DeepLabCut +Lauer, J., Zhou, M., Ye, S., Menegas, W., Schneider, S., Nath, T., Rahman, M.M., +Di Santo, V., Soberanes, D., Feng, G., Murthy, V.N., Lauder, G.V., Dulac, C., +Mathis, M.W., & Mathis, A. (2022). +Nature Methods, 19, 496 - 504. + +Conceptualization was done by A.M. and M.W.M. Formal analysis and code were done by J.L., A.M. and M.W.M. +New deep architectures were designed by M.Z., S.Y. and A.M. GUIs were done by J.L., M.W.M. and T.N. +Benchmark was set by S.S., M.W.M., A.M. and J.L. Marmoset data were gathered by W.M. and G.F. +Marmoset behavioral analysis was carried out by W.M. Parenting data were gathered by M.M.R., A.M. and C.D. +Tri-mouse data were gathered by D.S., A.M. and V.N.M. Fish data were gathered by V.D.S. and G.L. +The article was written by A.M., M.W.M. and J.L. with input from all authors. +M.W.M. and A.M. co-supervised the project. + +############################################################################################################ + +DeepLabCut 2.2 - 3.0 additions: +A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG +M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab +J Lauer, jessy@deeplabcut.org | https://github.com/jeylau +N Poulsen, neils.poulsen@epfl.ch | https://github.com/n-poulsen +S Schneider, stes@hey.com | https://github.com/stes +S Ye, shaokai.ye@epfl.ch | https://github.com/yeshaokai + +Preprint: +Ye, S., Filippova, A., Lauer, J., Schneider, S., Vidal, M., Qiu, T., Mathis, A., & Mathis, M.W. (2023). +SuperAnimal pretrained pose estimation models for behavioral analysis. https://arxiv.org/abs/2203.07436 + + +############################################################################################################ + +DeepLabCut 3.0 Toolbox +M Mathis, mackenzie@post.harvard.edu | https://github.com/MMathisLab +A Mathis, alexander.mathis@epfl.ch | https://github.com/AlexEMG +N Poulsen, neils.poulsen@epfl.ch | https://github.com/n-poulsen +S Ye, shaokai.ye@epfl.ch | https://github.com/yeshaokai +A Filippova, anastasiia.filippova@epfl.ch | https://github.com/nastya236 +Q Macé | https://github.com/QuentinJGMace +J Lauer, jessy@deeplabcut.org | https://github.com/jeylau +L Stoffl, lucas.stoffl@epfl.ch | https://github.com/LucZot + +We also greatly thank the 2023 DeepLabCut AI Residents who contributed: +Anna Teruel-Sanchis | https://github.com/anna-teruel +Riza Rae Pineda | https://github.com/rizarae-p +Konrad Danielewski | https://github.com/KonradDanielewski + +Products: +PyTorch backend for DeepLabCut +Expanded SuperAnimal capabilities +New model architectures (WIP: stay tuned, but includes BUCTD) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f4396c3d16..b26ffb0fb0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal +level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at alexander.mathis@bethgelab.org. All +reported by contacting the project team at alexander.mathis@epfl.ch. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99eb468d03..79f60b0674 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,73 +1,153 @@ -# How to Contribute to DeepLabCut +# Contributing to DeepLabCut -DeepLabCut is an actively developed package and we welcome community development and involvement. We are especially seeking people from underrepresented backgrounds in OSS to contribute their expertise and experience. Please get in touch if you want to discuss specific contributions you are interested in developing, and we can help shape a road-map. +Thanks for your interest in contributing to DeepLabCut! We welcome bug fixes, new features, documentation improvements, tests, and general maintenance contributions. -We are happy to receive code extensions, bug fixes, documentation updates, etc. +We especially encourage contributions from people from backgrounds that are underrepresented in open-source software. If you want to discuss an idea before opening a pull request, feel free to start a discussion or open an issue. -If you are a new user, we recommend checking out the detailed [Github Guides](https://guides.github.com). +If you are new to GitHub, the [GitHub Guides](https://guides.github.com/) are a great place to start. -## Setting up a development installation +## Ways to contribute -In order to make changes to `deeplabcut`, you will need to [fork](https://guides.github.com/activities/forking/#fork) the -[repository](https://github.com/deeplabcut/deeplabcut). +You can help by: -If you are not familiar with `git`, we recommend reading up on [this guide](https://guides.github.com/introduction/git-handbook/#basic-git). +- Fixing bugs +- Improving documentation +- Adding tests +- Improving examples +- Refactoring or cleaning up code +- Proposing or implementing new features -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! +## Development setup -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. +To work on DeepLabCut locally: -**Step 1:** +1. [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). +2. Clone your fork: -- git clone the repo into a folder on your computer: +```bash +git clone https://github.com//DeepLabCut.git +cd DeepLabCut +``` -- click on this green button and copy the link: +3. Create and activate a Python environment. -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581984907363-G8AFGX4V20Y1XD1PSZAK/ke17ZwdGBToddI8pDm48kGJBV0_F4LE4_UtCip_K_3lZw-zPPgdn4jUwVcJE1ZvWEtT5uBSRWt4vQZAgTJucoTqqXjS3CfNDSuuf31e0tVE0ejQCe16973Pm-pux3j5_Oqt57D2H0YbaJ3tl8vn_eR926scO3xePJoa6uVJa9B4/gitclone.png?format=500w) +We recommend using the project's development dependency group from `pyproject.toml` so you get the tools needed for local development (including formatting, linting, and testing). -- then in the terminal type: `git clone https://github.com/DeepLabCut/DeepLabCut.git` +For example, with `uv`: -**Step 2:** +```bash +uv sync --group dev +``` -- Now you will work from the terminal inside this cloned folder: +With `pip` (e.g. in a `conda` environment): -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581985288123-V8XUAY0C0ZDNJ5WBHB7Y/ke17ZwdGBToddI8pDm48kIsGBOdR9tS_SxF6KQXIcDtZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpz3c8X74DzCy4P3pv-ZANOdh-3ZL9iVkcryTbbTskaGvEc42UcRKU-PHxLXKM6ZekE/terminal.png?format=750w) +```bash +pip install -e . --group dev +``` -- Now, when you start `ipython` and `import deeplabcut` you are importing the folder "deeplabcut" - so any changes you make, or any changes we made before adding it to the pip package, are here. +If you use a different environment manager, install the package in editable/development mode together with the `dev` dependency group defined in `pyproject.toml`. -- You can also check which deeplabcut you are importing by running: `deeplabcut.__file__` +## Working on the code -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1581985466026-94OCSZJ5TL8U52JLB5VU/ke17ZwdGBToddI8pDm48kNdOD5iqmBzHwUaWGKS6qHBZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpyQPoegsR7K4odW9xcCi1MIHmvHh95_BFXYdKinJaRhV61R4G3qaUq94yWmtQgdj1A/importlocal.png?format=750w) +Once your environment is ready, your local checkout is what Python will import. -If you make changes to the code/first use the code, be sure you run `./resinstall.sh`, which you find in the main DeepLabCut folder: +If you want to verify that you are using the local source tree, you can run: -![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1609353210708-FRNREI7HUNS4GLDSJ00G/ke17ZwdGBToddI8pDm48kAya1IcSd32bok4WHvykeicUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYy7Mythp_T-mtop-vrsUOmeInPi9iDjx9w8K4ZfjXt2dq18t0tDkB2HMfL2JGcLHN27k5rSOPIU8nEAZT0p1MiSCjLISwBs8eEdxAxTptZAUg/Screen+Shot+2020-12-30+at+7.33.16+PM.png?format=2500w) +```bash +python -c "import deeplabcut; print(deeplabcut.__file__)" +``` +Using `ipython` or Jupyter is completely optional—use whatever workflow you prefer. +If you change packaged resources or otherwise need to refresh the local installation, run: -Note, before committing to DeepLabCut, please be sure your code is formatted according to `black`. To learn more, -see [`black`'s documentation](https://black.readthedocs.io/en/stable/). +```bash +./reinstall.sh +``` -Now, please make a [pull request](https://github.com/DeepLabCut/DeepLabCut/pull/new/) that includes both a **summary of and changes to**: +> [!NOTE] +> This script automatically uninstalls the package, builds a new wheel using `setup.py`, and installs that wheel. It is not a simple `pip install -e .` because some resources are copied during installation and need to be refreshed. -- 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) idealy you provide that functionality as well. If you have any questions, please reach out: admin@deeplabcut.org +## Code style and pre-commit -**TestScript outputs:** +We use `pre-commit` to run formatting and other checks before code is committed. -- The **OS it has been tested on** -- the **output of the [testscript.py](/examples/testscript.py)** and if you are editing the **3D code the [testscript_3d.py](/examples/testscript_3d.py)**, and if you edit multi-animal code please run the [maDLC test script](https://github.com/DeepLabCut/DeepLabCut/blob/master/examples/testscript_multianimal.py). +Set it up once in your clone: -**Review & Formatting:** +```bash +pre-commit install +``` -- 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)) +Whenever you commit, `pre-commit` will run the configured checks. +Please run `pre-commit` before opening a pull request. This helps catch formatting, import ordering, whitespace, YAML, and other common issues early and accelerates code review greatly. -**DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals:** +## Tests -- the [authors](/AUTHORS) -- [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) +Pull requests are validated in CI, and contributors are encouraged to run tests locally using: +```bash +pytest tests +``` +in the project root before opening a pull request. + +> [!IMPORTANT] +> Heavier tests are also run automatically on GitHub, so this is not a strict requirement, +> but it can help catch issues early and speed up the review process. + +## Pull request guidelines + +When submitting a pull request, please: + +- Clearly describe what changed and why +- Link any related issue(s) +- Update docstrings and documentation when behavior changes +- Add or update tests when appropriate +- Include a small usage example when it helps reviewers understand and/or test the change + +Smaller, focused pull requests are usually much easier to review than very large ones. + +### Draft pull requests + +We use draft pull requests to indicate work in progress. +You may still request reviews and feedbacks on draft pull requests, and we encourage you to do so if you would like early feedback on your work. +Please note that the draft status is in no way related to the perceived quality of the code or its potential for merging, but is simply a way to indicate that the work is not yet ready for final review and merging. +Most pull requests exist for the majority of their lifetime as drafts, which is expected. + +## Documentation + +Documentation improvements are always welcome. + +If your change affects users, please update the relevant docs, examples, or inline docstrings so the behavior is discoverable and easy to understand. + +## Code headers and notices + +If you need to standardize code headers, run: + +```bash +python tools/update_license_headers.py +``` + +Contributors are requested not to update `NOTICE.yml` or `LICENSE` files. + +## Review process + +A maintainer will review your pull request. You do not need to supply a specific release timeline in your PR description—contributions are reviewed and merged as capacity allows. + +If you have questions about where a change should go or how to structure it, opening a draft pull request is completely fine. + +## Need help? + +If you are unsure whether something is in scope, open an issue or draft PR and ask. +We'd much rather help early than have you spend time on the wrong thing. +We also welcome "Feature requests" issues if you would like to discuss implementation details or would like preliminary feedback. + +## Acknowledgments + +DeepLabCut is an open-source project and has benefited from many contributors over time, including: + +- The [authors](/AUTHORS) +- Listed [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) +- And many others over the years. + +We look forward to your contributions! 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 new file mode 100644 index 0000000000..d2ced97e0f --- /dev/null +++ b/NOTICE.yml @@ -0,0 +1,115 @@ +# Main repository license +- header: | + DeepLabCut Toolbox (deeplabcut.org) + © A. & M.W. 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 + include: + - 'deeplabcut/**/*.py' + - 'tests/**/*.py' + - 'examples/**/*.py' + - 'docs/**/*.py' + #- 'conda-environments/**/*.yaml' + +# License for files adapted from DeeperCut by Eldar Insafutdinov +# https://github.com/eldar/pose-tensorflow + +# Applies to most files in deeplabcut.pose_estimation_tensorflow +- header: | + DeepLabCut Toolbox (deeplabcut.org) + © A. & M.W. Mathis Labs + https://github.com/DeepLabCut/DeepLabCut + + Please see AUTHORS for contributors. + https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS + + Adapted from DeeperCut by Eldar Insafutdinov + https://github.com/eldar/pose-tensorflow + + Licensed under GNU Lesser General Public License v3.0 + include: + # This filelist was generated by running + # find deeplabcut/pose_estimation_tensorflow -iname '*.py' | xargs grep 'Eldar Insafutdinov' + # from the repo base directory. + - deeplabcut/pose_estimation_tensorflow/config.py + - deeplabcut/pose_estimation_tensorflow/datasets/factory.py + - deeplabcut/pose_estimation_tensorflow/vis_dataset.py + - deeplabcut/pose_estimation_tensorflow/core/train.py + - deeplabcut/pose_estimation_tensorflow/core/predict.py + - deeplabcut/pose_estimation_tensorflow/core/test.py + - deeplabcut/pose_estimation_tensorflow/default_config.py + - deeplabcut/pose_estimation_tensorflow/util/visualize.py + - deeplabcut/pose_estimation_tensorflow/util/__init__.py + - deeplabcut/pose_estimation_tensorflow/util/logging.py + - deeplabcut/pose_estimation_tensorflow/nnets/resnet.py + - deeplabcut/pose_estimation_tensorflow/__init__.py + exclude: [] + +# Tensorflow licenses +- header: | + Copyright 2019 The TensorFlow Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + include: + - deeplabcut/pose_estimation_tensorflow/backbones/*.py + - deeplabcut/pose_estimation_tensorflow/nnets/utils.py + +- header: | + Copyright 2018 The TensorFlow Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + include: + - deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py + - deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py + - deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py + +# TIMM license +- header: | + Copyright 2019 Ross Wightman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Hacked together by / Copyright 2020 Ross Wightman + https://github.com/rwightman/pytorch-image-models/blob/main/timm/scheduler/scheduler_factory.py + include: + - deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py + - deeplabcut/pose_tracking_pytorch/model/backones/vit_pytorch.py + +# PyTorch license + +- header: | + See https://github.com/pytorch/pytorch/blob/main/LICENSE diff --git a/README.md b/README.md index b89160b0e3..3c2ca193cc 100644 --- a/README.md +++ b/README.md @@ -1,217 +1,258 @@ -Code style: black -![Python package](https://github.com/DeepLabCut/DeepLabCut/workflows/Python%20package/badge.svg) -[![PyPI version](https://badge.fury.io/py/deeplabcut.svg)](https://badge.fury.io/py/deeplabcut) -[![Downloads](https://pepy.tech/badge/deeplabcut)](https://pepy.tech/project/deeplabcut) -[![Downloads](https://pepy.tech/badge/deeplabcut/month)](https://pepy.tech/project/deeplabcut) -[![GitHub stars](https://img.shields.io/github/stars/AlexEMG/DeepLabCut.svg?style=social&label=Star)](https://github.com/AlexEMG/DeepLabCut) +
-[![Generic badge](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](CONTRIBUTING.md) -[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) -[![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) -[![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://twitter.com/DeepLabCut) -[![GitHub forks](https://img.shields.io/github/forks/AlexEMG/DeepLabCut.svg?style=social&label=Fork)](https://github.com/AlexEMG/DeepLabCut)

- -

-

- www.deeplabcut.org +

- + - - + - -

-DeepLabCut is a toolbox for markerless pose estimation of animals performing various tasks. [Read a short development and application summary below](https://github.com/AlexEMG/DeepLabCut#why-use-deeplabcut). As long as you can see (label) what you want to track, you can use this toolbox, as it is animal and object agnostic. + + + +[📚Documentation](https://deeplabcut.github.io/DeepLabCut/README.html) | +[🛠️ Installation](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) | +[🌎 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) + + +[🫶 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) +[![Downloads](https://pepy.tech/badge/deeplabcut)](https://pepy.tech/project/deeplabcut) +[![Downloads](https://pepy.tech/badge/deeplabcut/month)](https://pepy.tech/project/deeplabcut) +[![PyPI version](https://badge.fury.io/py/deeplabcut.svg)](https://badge.fury.io/py/deeplabcut) +[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) +Code style: black +[![GitHub stars](https://img.shields.io/github/stars/DeepLabCut/DeepLabCut.svg?style=social&label=Star)](https://github.com/DeepLabCut/DeepLabCut) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/deeplabcut/deeplabcut.svg)](http://isitmaintained.com/project/deeplabcut/deeplabcut "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/deeplabcut/deeplabcut.svg)](http://isitmaintained.com/project/deeplabcut/deeplabcut "Percentage of issues still open") +[![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) +[![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) +[![Generic badge](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](CONTRIBUTING.md) +[![CZI's Essential Open Source Software for Science](https://chanzuckerberg.github.io/open-science/badges/CZI-EOSS.svg)](https://czi.co/EOSS) + +
+ +# 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). + +# [Installation](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) -**Latest updates:** +Please click the link above for all the information you need to get started! Please note that currently we support only Python 3.10+ (see conda files for guidance). -:purple_heart: DeepLabCut supports multi-animal pose estimation (**update:** maDLC is out of beta mode and depreciated, thanks to the testers out there! Your labeled data will be backwards compatible, but not all other steps. Please see the new `2.2rc1` release). +## Quick start -:purple_heart: We have a real-time package available! http://DLClive.deeplabcut.org +Developers Stable Release: very quick start (Python 3.10+ required) to install +DeepLabCut with the PyTorch engine -# [Installation: how to install DeepLabCut](docs/installation.md) +- [1] [Install PyTorch](https://pytorch.org/get-started/locally/) (**install and then select the desired +CUDA version if you want to use a GPU**): `pip install torch torchvision`. +Or as an example for GPU support (please check pytorch docs to get the perfect version for your CUDA): +```bash +conda install pytorch cudatoolkit=11.3 -c pytorch +``` +- [2] Then, install `DeepLabCut` (with all functions + the GUI): -Very quick start: `pip install 'deeplabcut[gui]'` that includes all GUI functions, or `pip install deeplabcut` (headless version) -* you also need TensorFlow (1.x currently), therefore we recommend using our conda files, see [here](https://github.com/DeepLabCut/DeepLabCut/blob/master/conda-environments/README.md) +```bash +pip install --pre "deeplabcut[gui]" +``` +or `pip install --pre "deeplabcut"` (headless +version with PyTorch)! -# [Documentation: The DeepLabCut Process](docs/README.md) +To use the TensorFlow (TF) engine: you'll need to run `pip install "deeplabcut[gui,tf]"` or `pip install "deeplabcut[tf]"` (headless version with TF). Alternatively, we also offer more targeted optional TensorFlow installs for specific CUDA setups, e.g. `deeplabcut[tf-cu11]` or `deeplabcut[tf-cu12]`. Please refer to our [installation instructions](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) for more detailed information on Python version, CUDA compatibility, etc. +We aim to **deprecate the tensorflow backend** in version 3.2 (release date TBD). -An overview of the pipeline and workflow for project management. For a step-by-step user guide, please also read the [Nature Protocols paper](https://doi.org/10.1038/s41596-019-0176-0)! -For a deeper understanding and more resources for you to get started with Python and DeepLabCut, please check out our free online course! http://DLCcourse.deeplabcut.org + +# Documentation: The DeepLabCut Process + +Our docs walk you through using DeepLabCut, and key API points. For an overview of the toolbox and workflow for project management, see our step-by-step at [Nature Protocols paper](https://doi.org/10.1038/s41596-019-0176-0). + + +

-# [DEMO the code](/examples) +# [Code demo](examples/README.md) + +🐭 Pose tracking of single animals demo [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb) -We provide data and several Jupyter Notebooks: one that walks you through a demo dataset to test your installation, and another Notebook to run DeepLabCut from the beginning on your own data. We also show you how to use the code in Docker, and on Google Colab. +See [more demos here](https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/README.md). +We provide data and several Jupyter Notebooks, walking you through a demo dataset to test your installation, and another to run DeepLabCut from the start on your own data. +We also show how to use the code in Docker, and on Google Colab. # Why use DeepLabCut? -In 2018, we demonstrated the capabilities for [trail tracking](https://vnmurthylab.org/), [reaching in mice](http://www.mousemotorlab.org/) and various Drosophila behaviors during egg-laying (see [Mathis et al.](https://www.nature.com/articles/s41593-018-0209-y) for details). There is, however, nothing specific that makes the toolbox only applicable to these tasks and/or species. The toolbox has already been successfully applied (by us and others) to [rats](http://www.mousemotorlab.org/deeplabcut), humans, various fish species, bacteria, leeches, various robots, cheetahs, [mouse whiskers](http://www.mousemotorlab.org/deeplabcut) and [race horses](http://www.mousemotorlab.org/deeplabcut). DeepLabCut utilizes the feature detectors (ResNets + readout layers) of one of the state-of-the-art algorithms for human pose estimation by Insafutdinov et al., called DeeperCut, which inspired the name for our toolbox (see references below). Furthermore, we have added faster variants with MobileNetV2 backbones (see [Pretraining boosts out-of-domain robustness for pose estimation](https://arxiv.org/abs/1909.11229)). Additionally, we have improved the inference speed and provided additional augmentation methods (via tensorpack and imgaug), and added real-time and mutli-animal support, and new neural networks optimized for animal pose estimation. +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. + +## Pretrained Models + +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 dataset](https://doi.org/10.5281/zenodo.10619172) with *AP-10K* held out for out-of-domain testing and the [SuperAnimal-TopViewMouse dataset](https://doi.org/10.5281/zenodo.13757509) with *DLC-openfield* held out for out-of-distribution testing (see [Ye et al. 2024](https://www.nature.com/articles/s41467-024-48792-2)). +We provide models that include AP-10K in the API (and GUI). +Note, there are many different models to select from in DeepLabCut 3.0. We strongly recommend you check [this Guide](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/architectures.html) for more details. +This table, and those below, give you a sense of performance in real-world complex in-the-wild and lab mouse data, respectively. +This [link provides the model weights](https://huggingface.co/mwmathis/DeepLabCutModelZoo-SuperAnimal-Quadruped) to reproduce the numbers; but please note, our `full` models are in our DLClibrary and released in the API. + +
DLC 3.0 Pose Estimation (Top Down Models) + +| Model Name | Type | mAP SA-Q on AP-10K | mAP SA-TVM on DLC-OpenField | +|------------------------------|------------|---------------------|-----------------------------| +| top_down_resnet_50 | Top-Down | 54.9 | 93.5 | +| top_down_resnet_101 | Top-Down | 55.9 | 94.1 | +| top_down_hrnet_w32 | Top-Down | 52.5 | 92.4 | +| top_down_hrnet_w48 | Top-Down | 55.3 | 93.8 | +| rtmpose_s | Top-Down | 52.9 | 92.9 | +| rtmpose_m | Top-Down | 55.4 | 94.8 | +| rtmpose_x | Top-Down | 57.6 | 94.5 | +
+ +## The History + +### Development and Applications + +In 2018, we demonstrated the capabilities for [trail tracking](https://vnmurthylab.org/), [reaching in mice](http://www.mousemotorlab.org/) and various Drosophila behaviors during egg-laying (see [Mathis et al.](https://www.nature.com/articles/s41593-018-0209-y) for details). There is, however, nothing specific that makes the toolbox only applicable to these tasks and/or species. +The toolbox has already been successfully applied (by us and others) to [rats](http://www.mousemotorlab.org/deeplabcut), humans, various fish species, bacteria, leeches, various robots, cheetahs, [mouse whiskers](http://www.mousemotorlab.org/deeplabcut) and [race horses](http://www.mousemotorlab.org/deeplabcut). +DeepLabCut utilized the feature detectors (ResNets + readout layers) of one of the state-of-the-art algorithms for human pose estimation by Insafutdinov et al., called DeeperCut, which inspired the name for our toolbox (see references below). Since this time, the package has changed substantially. +The code has been re-tooled and re-factored since 2.1+: We have added faster and higher performance variants with MobileNetV2s, EfficientNets, and our own DLCRNet backbones (see [Pretraining boosts out-of-domain robustness for pose estimation](https://arxiv.org/abs/1909.11229) and [Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0)). Additionally, we have improved the inference speed and provided both additional and novel augmentation methods, added real-time, and multi-animal support. +In v3.0+ we have updated the backend to support PyTorch. This brings not only an easier installation process for users, but performance gains, developer flexibility, and a lot of new tools! Importantly, the high-level API stays the same, so it will be a seamless transition for users 💜! +We currently provide state-of-the-art performance for animal pose estimation and the labs (M. Mathis Lab and A. Mathis Group) have both top journal and computer vision conference papers.

- +

**Left:** Due to transfer learning it requires **little training data** for multiple, challenging behaviors (see [Mathis et al. 2018](https://www.nature.com/articles/s41593-018-0209-y) for details). **Mid Left:** The feature detectors are robust to video compression (see [Mathis/Warren](https://www.biorxiv.org/content/early/2018/10/30/457242) for details). **Mid Right:** It allows 3D pose estimation with a single network and camera (see [Mathis/Warren](https://www.biorxiv.org/content/early/2018/10/30/457242)). **Right:** It allows 3D pose estimation with a single network trained on data from multiple cameras together with standard triangulation methods (see [Nath* and Mathis* et al. 2019](https://doi.org/10.1038/s41596-019-0176-0)). -**DeepLabCut** is embedding in a larger open-source eco-system, providing behavioral tracking for neuroscience, ecology, medical, and technical applications. Moreover, many new tools are being actively developed. See [DLC-Utils](https://github.com/DeepLabCut/DLCutils) for some helper code. +### Ecosystem + +**DeepLabCut** is part of a larger open-source eco-system, providing behavioral tracking for neuroscience, ecology, medical, and technical applications. +Moreover, many new tools are being actively developed. See [DLC-Utils](https://github.com/DeepLabCut/DLCutils) for some helper code.

-## Code contributors: +### Code contributors + +DeepLabCut was originally developed by [Alexander Mathis](https://github.com/AlexEMG) & [Mackenzie Mathis](https://github.com/MMathisLab), and was extended in 2.0 with the core dev team consisting of [Tanmay Nath](https://github.com/meet10may) (2.0-2.1), [Jessy Lauer](https://github.com/jeylau) (2.1-2.4), and [Niels Poulsen](https://github.com/n-poulsen) (2.3-3.0). +DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals including early contributors: Mert Yuksekgonul, Tom Biasi, Richard Warren, Ronny Eichler, Hao Wu, Federico Claudi, Gary Kane and Jonny Saunders as well as the [100+ contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors). +Please see [AUTHORS](https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS) for more details! -DLC code was originally developed by [Alexander Mathis](https://github.com/AlexEMG) & [Mackenzie Mathis](https://github.com/MMathisLab), and was extended in 2.0 with [Tanmay Nath](http://www.mousemotorlab.org/team), and currently actively developed with our CZI DLC Fellow, [Jessy Lauer](https://github.com/jeylau). The original feature detector code is based on Eldar Insafutdinov's TensorFlow implementation of [DeeperCut](https://github.com/eldar/pose-tensorflow). DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals including Mert Yuksekgonul, Tom Biasi, Richard Warren, Ronny Eichler, Hao Wu, Federico Claudi, Gary Kane and Jonny Saunders as well as the [contributors](https://github.com/AlexEMG/DeepLabCut/graphs/contributors). Please see [AUTHORS](https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS) for more details! - -This is an actively developed package and we welcome community development and involvement. +🤩 This is an actively developed package and we welcome community development and involvement: -## Community Support, Developers, & Help: +[![Contributors](https://contrib.rocks/image?repo=DeepLabCut/DeepLabCut)](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) -- We are a community partner on the [![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). Please post help and support questions on the forum with the tag DeepLabCut. Check out their mission statement [Scientific Community Image Forum: A discussion forum for scientific image software](https://journals.plos.org/plosbiology/article?id=10.1371/journal.pbio.3000340). -- If you encounter a previously unreported bug/code issue, please post here (we encourage you to search issues first): https://github.com/DeepLabCut/DeepLabCut/issues +# Get Assistance & be part of the DLC Community✨ -- For quick discussions amongst users, please see here: [![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +| 🚉 Platform | 🎯 Goal | ⏱️ Estimated Response Time | 📢 Support Squad | +|------------------------------------------------------------|-----------------------------------------------------------------------------|---------------------------|----------------------------------------| +| GitHub - [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 - [Contributing](https://github.com/DeepLabCut/DeepLabCut/blob/master/CONTRIBUTING.md) | To contribute your expertise and experience🙏💯 | 2-5 days | DLC Core Dev Team | +| [![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 | +| [![BlueSky](https://img.shields.io/badge/BlueSky-%40deeplabcut-blue?logo=bluesky)](https://bsky.app/profile/deeplabcut.bsky.social) | To keep up with our latest news and updates 📢 | 2-5 days | DLC Team | +| [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) | To keep up with our latest news and updates 📢 | 2-5 days | DLC Team | + + -- If you want to contribute to the code, please read our guide [here!](CONTRIBUTING.md) -- The project [road map](docs/roadmap.md). Get in touch with us if you want to help! +## References \& Citations -## References: +Please see our [dedicated page](https://deeplabcut.github.io/DeepLabCut/docs/citation.html) on how to **cite DeepLabCut** 🙏 and our suggestions for your Methods section! -If you use this code or data we kindly as that you please [cite Mathis et al, 2018](https://www.nature.com/articles/s41593-018-0209-y) and, if you use the Python package (DeepLabCut2.x) please also cite [Nath, Mathis et al, 2019](https://doi.org/10.1038/s41596-019-0176-0). If you utilize the MobileNetV2s or EfficientNets please cite [Mathis, Biasi et al. 2021](https://openaccess.thecvf.com/content/WACV2021/papers/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.pdf). If you use multi-animal in beta mode, please contact us; if you use the 2.2rc1+, please cite Lauer et al. 2021. +## License -DOIs (#ProTip, for helping you find citations for software, check out [CiteAs.org](http://citeas.org/)!): +This project is primarily licensed under the GNU Lesser General Public License v3.0. Note that the software is provided "as is", without warranty of any kind, express or implied. If you use the code or data, please cite us! Note, artwork (DeepLabCut logo) and images are copyrighted; please do not take or use these images without written permission. -- Mathis et al 2018: [10.1038/s41593-018-0209-y](https://doi.org/10.1038/s41593-018-0209-y) -- Nath, Mathis et al 2019: [10.1038/s41596-019-0176-0](https://doi.org/10.1038/s41596-019-0176-0) +SuperAnimal models are provided for research use only (non-commercial use). +## Major Versions -Please check out the following references for more details: +**For all versions, please see [here](https://github.com/DeepLabCut/DeepLabCut/releases).** - @article{Mathisetal2018, - title={DeepLabCut: markerless pose estimation of user-defined body parts with deep learning}, - author = {Alexander Mathis and Pranav Mamidanna and Kevin M. Cury and Taiga Abe and Venkatesh N. Murthy and Mackenzie W. Mathis and Matthias Bethge}, - journal={Nature Neuroscience}, - year={2018}, - url={https://www.nature.com/articles/s41593-018-0209-y}} +VERSION 3.0: A whole new experience with PyTorch🔥. While the high-level API remains the same, the backend and developer friendliness have greatly improved, along with performance gains! - @article{NathMathisetal2019, - title={Using DeepLabCut for 3D markerless pose estimation across species and behaviors}, - author = {Nath*, Tanmay and Mathis*, Alexander and Chen, An Chi and Patel, Amir and Bethge, Matthias and Mathis, Mackenzie W}, - journal={Nature Protocols}, - year={2019}, - url={https://doi.org/10.1038/s41596-019-0176-0}} +VERSION 2.3: Model Zoo SuperAnimals, and a whole new GUI experience. - @article{insafutdinov2016eccv, - title = {DeeperCut: A Deeper, Stronger, and Faster Multi-Person Pose Estimation Model}, - author = {Eldar Insafutdinov and Leonid Pishchulin and Bjoern Andres and Mykhaylo Andriluka and Bernt Schiele}, - booktitle = {ECCV'16}, - url = {http://arxiv.org/abs/1605.03170}} +VERSION 2.2: Multi-animal pose estimation, identification, and tracking with DeepLabCut is supported (as well as single-animal projects). - @article{Mathis2020DeepLT, - title={Deep learning tools for the measurement of animal behavior in neuroscience}, - author={Mackenzie W. Mathis and Alexander Mathis}, - journal={Current Opinion in Neurobiology}, - year={2020}, - volume={60}, - pages={1-11}} +VERSION 2.0-2.1: This is the **Python package** of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) that was originally released in Oct 2018 with our [Nature Protocols](https://doi.org/10.1038/s41596-019-0176-0) paper (preprint [here](https://www.biorxiv.org/content/10.1101/476531v1)). +This package includes graphical user interfaces to label your data, and take you from data set creation to automatic behavioral analysis. It also introduces an active learning framework to efficiently use DeepLabCut on large experimental projects, and data augmentation tools that improve network performance, especially in challenging cases. - @article{Mathis2020Primer, - title={A Primer on Motion Capture with Deep Learning: Principles, Pitfalls, and Perspectives}, - author={Alexander Mathis and Steffen Schneider and Jessy Lauer and Mackenzie W. Mathis}, - journal={Neuron}, - year={2020}, - volume={108}, - pages={44-65}} +VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) can be found in the history of git, or here: https://github.com/DeepLabCut/DeepLabCut/releases/tag/1.11 -Our open-access pre-prints: +# News - @misc{Mathis2019_pretraining, - title={Pretraining boosts out-of-domain robustness for pose estimation}, - author={Alexander Mathis and Mert Y\"uksekg\"on\"ul and Byron Rogers and Matthias Bethge and Mackenzie W. Mathis}, - year={2019}, - eprint={1909.11229}, - archivePrefix={arXiv}, - primaryClass={cs.CV} - } +## Major releases - @article{NathMathis2018, - author = {Nath*, Tanmay and Mathis*, Alexander and Chen, An Chi and Patel, Amir and Bethge, Matthias and Mathis, Mackenzie W}, - title = {Using DeepLabCut for 3D markerless pose estimation across species and behaviors}, - year = {2018}, - doi = {10.1101/476531}, - publisher = {Cold Spring Harbor Laboratory}, - URL = {https://www.biorxiv.org/content/early/2018/11/24/476531}, - eprint = {https://www.biorxiv.org/content/early/2018/11/24/476531.full.pdf}, - journal = {bioRxiv} - } +💜 We released a major update, moving from 2.x --> 3.x with the backend change to PyTorch - @article{mathis2018markerless, - title={Markerless tracking of user-defined features with deep learning}, - author={Mathis, Alexander and Mamidanna, Pranav and Abe, Taiga and Cury, Kevin M and Murthy, Venkatesh N and Mathis, Mackenzie W and Bethge, Matthias}, - journal={arXiv preprint arXiv:1804.03142}, - year={2018} - } +💜 The DeepLabCut Model Zoo launches SuperAnimals, see more [here](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html). - @article{MathisWarren2018speed, - author = {Mathis, Alexander and Warren, Richard A.}, - title = {On the inference speed and video-compression robustness of DeepLabCut}, - year = {2018}, - doi = {10.1101/457242}, - publisher = {Cold Spring Harbor Laboratory}, - URL = {https://www.biorxiv.org/content/early/2018/10/30/457242}, - eprint = {https://www.biorxiv.org/content/early/2018/10/30/457242.full.pdf}, - journal = {bioRxiv} - } +💜 **DeepLabCut supports multi-animal pose estimation!** maDLC is out of beta/rc mode and beta is deprecated, thanks to the testers out there for feedback! Your labeled data will be backwards compatible, but not all other steps. Please see the [new `2.2+` releases](https://github.com/DeepLabCut/DeepLabCut/releases) for what's new & how to install it, please see our new [paper, Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0), and the [new docs]( https://deeplabcut.github.io/DeepLabCut) on how to use it! -## License: +💜 We support multi-animal re-identification, see [Lauer et al 2022](https://www.nature.com/articles/s41592-022-01443-0). -This project is licensed under the GNU Lesser General Public License v3.0. Note that the software is provided "as is", without warranty of any kind, express or implied. If you use the code or data, please cite us!. Note, artwork and images are copyrighted; please do not take or use these images without written permission. +💜 We have a **real-time** package available! [DLC-live on GitHub](https://github.com/DeepLabCut/DeepLabCut-live) and [DLC-live-GUI](https://github.com/DeepLabCut/DeepLabCut-live-GUI) -## Versions: +## In the news -VERSION 2.2: Multi-animal pose estimation and tracking with DeepLabCut. - -VERSION 2.0-2.1: This is the **Python package** of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) that was originally released with our [Nature Protocols](https://doi.org/10.1038/s41596-019-0176-0) paper (preprint [here](https://www.biorxiv.org/content/10.1101/476531v1)). -This package includes graphical user interfaces to label your data, and take you from data set creation to automatic behavioral analysis. It also introduces an active learning framework to efficiently use DeepLabCut on large experimental projects, and data augmentation tools that improve network performance, especially in challenging cases (see [panel b](https://camo.githubusercontent.com/77c92f6b89d44ca758d815bdd7e801247437060b/68747470733a2f2f737461746963312e73717561726573706163652e636f6d2f7374617469632f3537663664353163396637343536366635356563663237312f742f3563336663316336373538643436393530636537656563372f313534373638323338333539352f636865657461682e706e673f666f726d61743d37353077)). - -VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://www.nature.com/articles/s41593-018-0209-y) can be found in the history of git, or here: https://github.com/AlexEMG/DeepLabCut/releases/tag/1.11 - -## News (and in the news): - -- Jan 2021: [Pretraining boosts out-of-domain robustness for pose estimation](https://openaccess.thecvf.com/content/WACV2021/html/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.html) published in the IEEE Winter Conference on Applications of Computer Vision. We also added EfficientNet backbones to DeepLabCut, those are best trained with cosine decay (see paper). To use them, just pass "efficientnet-b0" to "efficientnet-b6" when creating the trainingset! +- 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! +- 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) +- May 2023: The Model Zoo is now fully integrated into the DeepLabCut GUI, making it easier than ever to access a variety of pre-trained models. Check out the accompanying paper: [SuperAnimal pretrained pose estimation models for behavioral analysis by Ye et al.](https://arxiv.org/abs/2203.07436) +- December 2022: DeepLabCut hits 450,000 downloads and 2.3 is the new stable release +- August 2022: DeepLabCut hit 400,000 downloads +- August 2021: 2.2 becomes the new stable release for DeepLabCut. +- July 2021: Docs are now at https://deeplabcut.github.io/DeepLabCut and we now include TensorFlow 2 support! +- May 2021: DeepLabCut hit 200,000 downloads! Also, Our preprint on 2.2, multi-animal DeepLabCut is released! +- Jan 2021: [Pretraining boosts out-of-domain robustness for pose estimation](https://openaccess.thecvf.com/content/WACV2021/html/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.html) published in the IEEE Winter Conference on Applications of Computer Vision. We also added EfficientNet backbones to DeepLabCut, those are best trained with cosine decay (see paper). To use them, just pass "`efficientnet-b0`" to "`efficientnet-b6`" when creating the trainingset! - Dec 2020: We released a real-time package that allows for online pose estimation and real-time feedback. See [DLClive.deeplabcut.org](http://DLClive.deeplabcut.org). - 5/22 2020: We released 2.2beta5. This beta release has some of the features of DeepLabCut 2.2, whose major goal is to integrate multi-animal pose estimation to DeepLabCut. - Mar 2020: Inspired by suggestions we heard at this weeks CZI's Essential Open Source Software meeting in Berkeley, CA we updated our [docs](docs/UseOverviewGuide.md). Let us know what you think! - Feb 2020: Our [review on animal pose estimation is published!](https://www.sciencedirect.com/science/article/pii/S0959438819301151) - Nov 2019: DeepLabCut was recognized by the Chan Zuckerberg Initiative (CZI) with funding to support this project. Read more in the [Harvard Gazette](https://news.harvard.edu/gazette/story/newsplus/harvard-researchers-awarded-czi-open-source-award/), on [CZI's Essential Open Source Software for Science site](https://chanzuckerberg.com/eoss/proposals/) and in their [Medium post](https://medium.com/@cziscience/how-open-source-software-contributors-are-accelerating-biomedicine-1a5f50f6846a) -- Oct 2019: DLC 2.1 released with lots of updates. In particular, a Project Manager GUI, MobileNetsV2, and augmentation packages (Imgaug and Tensorpack). For detailed updates see [releases](https://github.com/AlexEMG/DeepLabCut/releases) +- Oct 2019: DLC 2.1 released with lots of updates. In particular, a Project Manager GUI, MobileNetsV2, and augmentation packages (Imgaug and Tensorpack). For detailed updates see [releases](https://github.com/DeepLabCut/DeepLabCut/releases) - Sept 2019: We published two preprints. One showing that [ImageNet pretraining contributes to robustness](https://arxiv.org/abs/1909.11229) and a [review on animal pose estimation](https://arxiv.org/abs/1909.13868). Check them out! -- Jun 2019: DLC 2.0.7 released with lots of updates. For updates see [releases](https://github.com/AlexEMG/DeepLabCut/releases) -- Feb 2019: DeepLabCut joined [twitter](https://twitter.com/deeplabcut) [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://twitter.com/DeepLabCut) -- Jan 2019: We hosted workshops for DLC in Warsaw, Munich and Cambridge. The materials are available [here](https://github.com/AlexEMG/DeepLabCut-Workshop-Materials) +- Jun 2019: DLC 2.0.7 released with lots of updates. For updates see [releases](https://github.com/DeepLabCut/DeepLabCut/releases) +- Feb 2019: DeepLabCut joined [twitter](https://x.com/deeplabcut) [![Twitter Follow](https://img.shields.io/twitter/follow/DeepLabCut.svg?label=DeepLabCut&style=social)](https://x.com/DeepLabCut) +- Jan 2019: We hosted workshops for DLC in Warsaw, Munich and Cambridge. The materials are available [here](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials) - Jan 2019: We joined the Image Source Forum for user help: [![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) - Nov 2018: We posted a detailed guide for DeepLabCut 2.0 on [BioRxiv](https://www.biorxiv.org/content/early/2018/11/24/476531). It also contains a case study for 3D pose estimation in cheetahs. -- Nov 2018: Various (post-hoc) analysis scripts contributed by users (and us) will be gathered at [DLCutils](https://github.com/AlexEMG/DLCutils). Feel free to contribute! In particular, there is a script guiding you through +- Nov 2018: Various (post-hoc) analysis scripts contributed by users (and us) will be gathered at [DLCutils](https://github.com/DeepLabCut/DLCutils). Feel free to contribute! In particular, there is a script guiding you through importing a project into the new data format for DLC 2.0 - Oct 2018: new pre-print on the speed video-compression and robustness of DeepLabCut on [BioRxiv](https://www.biorxiv.org/content/early/2018/10/30/457242) - Sept 2018: Nature Lab Animal covers DeepLabCut: [Behavior tracking cuts deep](https://www.nature.com/articles/s41684-018-0164-y) @@ -220,3 +261,9 @@ importing a project into the new data format for DLC 2.0 - August 2018: NVIDIA AI Developer News: [AI Enables Markerless Animal Tracking](https://news.developer.nvidia.com/ai-enables-markerless-animal-tracking/) - July 2018: Ed Yong covered DeepLabCut and interviewed several users for the [Atlantic](https://www.theatlantic.com/science/archive/2018/07/deeplabcut-tracking-animal-movements/564338). - April 2018: first DeepLabCut preprint on [arXiv.org](https://arxiv.org/abs/1804.03142) + + # Funding + +We are grateful for the following support and funding over the years! +This software project was supported in part by the **Essential Open Source Software for Science (EOSS)** program at **Chan Zuckerberg Initiative** (cycles 1, 3, 3-DEI, 4), and jointly with the **Kavli Foundation** for **EOSS Cycle 6**! +We also thank the **Rowland Institute** at **Harvard** for funding from 2017-2020, and **EPFL** from 2020-present. diff --git a/_config.yml b/_config.yml index c4192631f2..b721b3aff9 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1,39 @@ -theme: jekyll-theme-cayman \ No newline at end of file +title: DeepLabCut +author: The DeepLabCut Team +logo: docs/images/logo.png +only_build_toc_files: true + +sphinx: + config: + autodoc_mock_imports: ["wx", "matplotlib", "qtpy", "PySide6", "napari", "shiboken6"] + mermaid_output_format: raw + html_static_path: ["docs/_static"] + html_css_files: ["custom.css"] + exclude_patterns: + - ".venv/**" + - "venv/**" + - "**/site-packages/**" + - "**/_build/**" + napoleon_google_docstring: true + napoleon_numpy_docstring: false + extra_extensions: + - sphinx.ext.napoleon + - sphinxcontrib.mermaid + +execute: + execute_notebooks: "off" + +html: + extra_navbar: "" + use_issues_button: true + use_repository_button: true + extra_footer: | +
Powered by Jupyter Book.
+ +repository: + url: https://github.com/DeepLabCut/DeepLabCut + path_to_book: docs + branch: main + +launch_buttons: + colab_url: "https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb" diff --git a/_toc.yml b/_toc.yml new file mode 100644 index 0000000000..b1844553dc --- /dev/null +++ b/_toc.yml @@ -0,0 +1,126 @@ +format: jb-book +root: README + +parts: + - caption: Getting started + chapters: + - file: docs/UseOverviewGuide + - file: docs/installation + sections: + # - file: docs/recipes/installTips + - file: docs/docker + # - file: docs/quick-start/index + # sections: + + - caption: Main workflows overview + chapters: + - file: docs/main-workflows/user-guide + sections: + - file: docs/quick-start/single_animal_quick_guide + - file: docs/quick-start/tutorial_maDLC + - file: docs/main-workflows/multi-animal-tracking + # - file: docs/standardDeepLabCut_UserGuide + # - file: docs/maDLC_UserGuide + - file: docs/Overviewof3D + + - caption: GUI workflow + chapters: + - file: docs/gui/PROJECT_GUI + sections: + - file: docs/beginner-guides/beginners-guide + - file: docs/beginner-guides/manage-project + - file: docs/beginner-guides/labeling + - file: docs/beginner-guides/Training-Evaluation + - file: docs/beginner-guides/video-analysis + - file: docs/gui/napari_GUI + sections: + - file: docs/gui/napari/basic_usage + - file: docs/gui/napari/advanced_usage + - file: docs/gui/napari/tracking/basic_usage + + - caption: Notebooks & Demos + chapters: + - file: docs/notebooks/your_data + sections: + - file: examples/COLAB/COLAB_YOURDATA_SuperAnimal + - file: examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis + - file: examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis + - file: docs/notebooks/main_demos + sections: + - file: examples/COLAB/COLAB_DEMO_SuperAnimal + - file: examples/COLAB/COLAB_DEMO_mouse_openfield + - file: examples/COLAB/COLAB_3miceDemo + - file: examples/COLAB/COLAB_HumanPose_with_RTMPose + - file: docs/notebooks/extra + sections: + - file: examples/COLAB/COLAB_transformer_reID + - file: examples/COLAB/COLAB_BUCTD_and_CTD_tracking + - file: examples/JUPYTER/Demo_3D_DeepLabCut + - file: examples/COLAB/COLAB_DLC_ModelZoo + + - caption: DeepLabCut 3.0 - PyTorch guides + chapters: + - file: docs/pytorch/index + sections: + - file: docs/pytorch/user_guide.md + - file: docs/pytorch/pytorch_config.md + - file: docs/pytorch/architectures.md + - file: docs/pytorch/Benchmarking_shuffle_guide + + - caption: Advanced, Performance & Live + chapters: + - file: docs/ModelZoo + sections: + - file: docs/recipes/UsingModelZooPupil + - file: docs/dlc-live/deeplabcutlive + - file: docs/dlc-live/dlc-live-gui/index + sections: + - file: docs/dlc-live/dlc-live-gui/quickstart/install + - file: docs/dlc-live/dlc-live-gui/user_guide/overview + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support + sections: + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing + sections: + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads + - file: docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format + - file: docs/benchmark + - file: docs/recipes/TechHardware + + - caption: Development + chapters: + - title: Developer Documentation + url: https://deeplabcut.github.io/DeepLabCut/dev/ + + - caption: Additional guides (Recipes) + chapters: + - file: docs/recipes/index + sections: + - file: docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook + - file: docs/HelperFunctions + - file: docs/convert_maDLC + - file: docs/recipes/OtherData + - file: docs/recipes/io + - file: docs/recipes/nn + - file: docs/recipes/post + - file: docs/recipes/BatchProcessing + - file: docs/recipes/DLCMethods + - file: docs/recipes/ClusteringNapari + - file: docs/recipes/OpenVINO + - file: docs/recipes/flip_and_rotate + - file: docs/recipes/pose_cfg_file_breakdown + # - file: docs/course + - file: docs/dlc-utils/index + sections: + - file: docs/dlc-utils/XROMM/usage + + - caption: Project & Community + chapters: + - file: docs/MISSION_AND_VALUES + - file: docs/roadmap + - file: docs/Governance + - file: CONTRIBUTING + - file: docs/citation diff --git a/changelog/3_0_0/images/buctd_benchmarks.png b/changelog/3_0_0/images/buctd_benchmarks.png new file mode 100644 index 0000000000..127b1d4f1b Binary files /dev/null and b/changelog/3_0_0/images/buctd_benchmarks.png differ diff --git a/changelog/3_0_0/images/openfield_benchmark_pr2613.png b/changelog/3_0_0/images/openfield_benchmark_pr2613.png new file mode 100644 index 0000000000..0ebedddb4c Binary files /dev/null and b/changelog/3_0_0/images/openfield_benchmark_pr2613.png differ diff --git a/changelog/3_0_0/images/speed_tensorflow.avif b/changelog/3_0_0/images/speed_tensorflow.avif new file mode 100644 index 0000000000..43dcf74443 Binary files /dev/null and b/changelog/3_0_0/images/speed_tensorflow.avif differ diff --git a/changelog/3_0_0/v3_0_0.md b/changelog/3_0_0/v3_0_0.md new file mode 100644 index 0000000000..994a65958b --- /dev/null +++ b/changelog/3_0_0/v3_0_0.md @@ -0,0 +1,158 @@ +# DeepLabCut 3.0: familiar workflows, modern foundations, better performance + +DeepLabCut 3.0 introduces a PyTorch-first training and inference stack while keeping the core DeepLabCut workflow familiar. +Projects still follow the same labeling, training, evaluation, and video-analysis pipeline used throughout the 2.x series, but the underlying engine has been substantially modernized. + +For users who have already been following the release candidates, many of these changes will already feel familiar. +DeepLabCut 3.0 consolidates these incremental changes into a stable release. + +## Increased model performance and speed + + + + + + + + +
Benchmark tableOpenfield benchmark results
Pose estimation performance of the 3.0 PyTorch models compared against previous TensorFlow models on the DeepLabCut Openfield dataset (see PR #2613); RMSE: root mean squared error. *Values from Mathis et al. 2018.
+ + + + + + + + + +
Trimice datasetSpeed comparison: PyTorch vs TensorFlow
Speed of the current PyTorch implementation (ResNet50) compared to the TensorFlow implementation. Results were obtained using a NVIDIA GeForce RTX 2080 Ti with CUDA 12.2 on the DeepLabCut Trimice dataset.
+ + + + + + + + +
BUCTD benchmarks
Comparison of the new BUCTD model architectures with DLCRNet and DEKR on the Marmoset, Fish and Trimice dataset. From Zhou et al., ICCV 2023.
+ +## The journey to 3.0 + +A quick recap of some of the major milestones leading to this release: + +- #2613 - Full initial PyTorch backend implementation +- #2952 - New bottom-up conditional top-down (BUCTD) model architecture +- #2795 - New RTMPose top-down architecture +- #2868 - Updated notebooks and Colab examples for PyTorch workflows +- #2804 - PyTorch model export + +And more, find the full PR reference [on GitHub](https://github.com/DeepLabCut/DeepLabCut/pulls?page=1&q=is%3Apr+label%3ADLC3.0%F0%9F%94%A5)! + +## Notable features in 3.0.0 + +### PyTorch-first, TensorFlow-compatible + +DeepLabCut 3.0 adds a new PyTorch backend while retaining TensorFlow support for legacy workflows. +Project management remains the same and labeled datasets remain compatible. +PyTorch models can be trained alongside previous TensorFlow models on the same train/test splits for direct benchmarking and comparison. + + +### Expanded architecture support + +DeepLabCut 3.0 significantly broadens the supported model ecosystem beyond the classic ResNet-based workflows. The PyTorch stack includes: + +- ResNet and HRNet backbones +- Bottom-up multi-animal approaches such as DEKR and PAF/DLCRNet variants +- Top-down detector-plus-pose pipelines including RTMPose +- Hybrid architectures such as BUCTD and CTD variants +- SuperAnimal-related pretrained workflows + +The documentation now includes dedicated [architecture guides](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/architectures.html) to help users choose models based on scene complexity and experimental needs. + +### Flexible PyTorch training configuration + +The PyTorch engine introduces a modern training stack with expanded augmentation options, training schedules, device management, and model architectures. For each training run, the settings are stored in a `pytorch_config.yaml`, enabling easy reproducibility. + +### Improved interoperability + +The new PyTorch data pipeline introduces loaders for both standard DeepLabCut projects and COCO-style datasets, making it easier to integrate DeepLabCut with broader computer-vision workflows and external annotation formats. + +### Model Zoo and SuperAnimal workflows + +DeepLabCut 3.0 continues to expand the Model Zoo and SuperAnimal ecosystem, making pretrained models and transfer learning more accessible. +Colab notebooks and updated GUI tooling make it easier to experiment with modern architectures without extensive setup. (see the [documentation](https://deeplabcut.github.io/DeepLabCut/README.html)) + +### Modernized installation and packaging + +The project has been moved to a newer packaging system, and is now based around pyproject.toml. This enables the use of modern package-managers & dependency resolvers, such as `uv` or `pdm`. +Users can still install only the components they require, be it GUI support, TensorFlow compatibility, ModelZoo features, and optional experimental integrations. + +### Labeling GUI + +DeepLabCut 3.0 is shipped with a new release of the napari-deeplabcut plugin. Our napari-based labeling GUI has undergone a major internal re-write and modernization: while preserving familiar UI and the DeepLabCut workflow, the update substantially improves stability, data handling, usability, visualization, and annotation workflows, now with automated point tracking for faster labeling. See the [release notes](https://github.com/DeepLabCut/napari-deeplabcut/releases) to find out about all improvements. + +### Upcoming: refreshed documentation + +We have updated and streamlined the documentation, with a focus on clarity and up-to-date information in core areas (installation, getting started guides, and more). +Expect the documentation to continue evolving soon after the release! + +## A major transition + +The jump from the final DeepLabCut 2.x releases to the current codebase is best understood as a transition to more recent Python & deep learning ecosystems rather than a routine update. +Taken together, the PyTorch backend, broader architecture support, ModelZoo integration, packaging modernization, updated labeling GUI, and documentation improvements represent a major evolution of DeepLabCut, which we are happy to release as 3.0. + + +## Closing thoughts + +We hope you enjoy this new version, and we aim to keep sharing many exciting improvements in the future in all areas, be it performance and speed, codebase quality improvements, foundation models integration, user experience and documentation. + +--- +## Changelog since 3.0.0rc14 + +- Add up-to-date uv.lock (#3242) +- Remove unnecessary imports (#3224) +- Add custom styling options for docs (custom.css) (#3207) +- Add internal helper for batched modelzoo inference from in-memory arrays (inference runner) (#3222) +- Implement intelligent test selection in CI (#3046) +- Revamp CONTRIBUTING.md (#3241) +- Update FMPose3D modelzoo integration (#3221) +- Add automated docs & notebooks freshness + normalization checks (#3228) +- Install from PyPI pre-release; add both-backends (#3238) +- Apply linting to entire codebase & add CI workflow to check linting (#3216) +- Bump requests from 2.32.5 to 2.33.0 (#3259) +- Refactor Analyze Videos tab (#3268) +- Consolidate test workflow infrastructure in CI (#3254) +- Move protobuf requirement to pyproject.toml (#3235) +- Use pinned ffmpeg version in CI (#3276) +- Docs versioning: Add glob support, better validation and reporting (#3278) +- Bump cryptography from 46.0.5 to 46.0.7 (#3277) +- Fix failing local Windows tests due to ruamel parsing (#3275) +- Update & de-duplicate skeleton builder (#3258) +- Bump pygments from 2.19.2 to 2.20.0 (#3262) +- update conda yaml: install pyside6 via conda instead of pip (#3253) +- Bump pyasn1 from 0.6.2 to 0.6.3 (#3249) +- Fix SuperAnimal / pretrained load for RTMPose: implement convert_weights on RTMCCHead (#3270) +- Use async update check in GUI (#3234) +- Update napari-DLC docs for refactor (#3280) +- Refactor/predict multianimal (#3220) +- Fix incorrect MultiLevel construction in outlier_frames.compute_deviations (#3247) +- Bump pillow from 12.1.1 to 12.2.0 (#3283) +- Bump pytest from 9.0.2 to 9.0.3 (#3284) +- CircleCI: disable hugginface xet (#3316) +- Update and diversify TensorFlow optional installations. (#3292) +- Bump urllib3 from 2.6.3 to 2.7.0 (#3325) +- Bump gitpython from 3.1.47 to 3.1.50 (#3322) +- make GenerativeSampler visibility-aware (#3305) +- Add isatty method to StreamWriter + eval GUI fix (#3314) +- Add conditional replacement of `@torch.inference_mode` for inference on AMD DirectML GPUs (#3295) +- Robustness fix: Annotation file not dropping likelihood column if present from machine labels (#3323) +- Remove trailing comma in models_to_framework.json (#3330) +- update `list_videos_in_folder` (#3303) +- Improve `TrainingDatasetMetadata` and `get_shuffle_engine` for incomplete projects (#3313) +- update RTMPose `SimCCPredictor`: expose `apply_softmax` and fix visibility thresholding (#3306) +- GUI: Add "Generate debug log" action (#3328) +- [Docker 1] Simplify and modernize Dockerfile (#3290) +- [Docker 2] Update deeplabcut-docker package (#3291) +- Add additional drop_likelihood_columns guards (#3333) +- Resolve inconsistent parameter names via aliasing + deprecationwarning (#3332) +- bump dlclibrary (v0.0.12) and napari-deeplabcut (v3.1.0) (#3338) diff --git a/compile.sh b/compile.sh deleted file mode 100644 index 88abbd852b..0000000000 --- a/compile.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -#cd to where DLC is installed, and then: dist-packages/deeplabcut/pose_estimation_tensorflow/lib/nms_cython -#i.e. in my case: -cd /usr/local/lib/python3.6/dist-packages/deeplabcut/pose_estimation_tensorflow/lib/nms_cython -python3 setup.py build_ext --inplace diff --git a/conda-environments/DEEPLABCUT.yaml b/conda-environments/DEEPLABCUT.yaml new file mode 100644 index 0000000000..4848f9a099 --- /dev/null +++ b/conda-environments/DEEPLABCUT.yaml @@ -0,0 +1,37 @@ +# DEEPLABCUT.yaml + +#DeepLabCut Toolbox (deeplabcut.org) +#© A. & M.W. 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 +# +# DeepLabCut environment +# +# FIRST: If you have an NVIDIA GPU and want to use it, check that you have drivers installed! +# To check if your GPUs are visible to PyTorch (and thus DeepLabCut), run: +# python -c "import torch; print(torch.cuda.is_available())" +# +# If "False" is printed, PyTorch (and thus DeepLabCut) cannot access your GPU. For +# more information, see: https://pytorch.org/get-started/locally/ +# +# install: conda env create -f DEEPLABCUT.yaml +# update: conda env update -f DEEPLABCUT.yaml +name: DEEPLABCUT +channels: + - conda-forge + - defaults +dependencies: + - python=3.10 + - pip + - ipython + - jupyter + - ffmpeg + - pyside6 + - pip: + - torch + - torchvision + - --pre + - deeplabcut[gui,modelzoo,wandb] diff --git a/conda-environments/DLC-CPU-LITE.yaml b/conda-environments/DLC-CPU-LITE.yaml deleted file mode 100644 index 8f7ede16fb..0000000000 --- a/conda-environments/DLC-CPU-LITE.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# DLC-CPU-LITE.yaml - -#DeepLabCut2.0 Toolbox (deeplabcut.org) -#© A. & M. Mathis Labs -#https://github.com/DeepLabCut/DeepLabCut -#Please see AUTHORS for contributors. -#Licensed under GNU Lesser General Public License v3.0 -# -# install: conda env create -f DLC-CPU-LITE.yaml -# update: conda env update -f DLC-CPU-LITE.yaml - -name: DLC-CPU-LITE -channels: - - conda-forge - - defaults -dependencies: - - python=3.7 - - pip - - jupyter - - nb_conda - - pip: - - tensorflow==1.15.5 - - deeplabcut diff --git a/conda-environments/DLC-CPU.yaml b/conda-environments/DLC-CPU.yaml deleted file mode 100644 index bacbbd654a..0000000000 --- a/conda-environments/DLC-CPU.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# DLC-CPU.yaml - -#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 -# -# install: conda env create -f DLC-CPU.yaml -# update: conda env update -f DLC-CPU.yaml - -name: DLC-CPU -channels: - - conda-forge - - defaults -dependencies: - - python=3.7 - - pip - - jupyter - - nb_conda - - pip: - - tensorflow==1.15.5 - - deeplabcut[gui] diff --git a/conda-environments/DLC-GPU-LITE.yaml b/conda-environments/DLC-GPU-LITE.yaml deleted file mode 100644 index 48ec803915..0000000000 --- a/conda-environments/DLC-GPU-LITE.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# DLC-GPU-LITE.yaml - -#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 -# -# DeepLabCut environment -# FIRST: INSTALL CORRECT DRIVER for GPU, see https://stackoverflow.com/questions/30820513/what-is-the-correct-version-of-cuda-for-my-nvidia-driver/30820690 -# -# install: conda env create -f DLC-GPU-LITE.yaml -# update: conda env update -f DLC-GPU-LITE.yaml -name: DLC-GPU-LITE -channels: - - conda-forge - - defaults -dependencies: - - python=3.7 - - pip - - cudnn=7 - - jupyter - - nb_conda - - Shapely - - pip: - - tensorflow-gpu==1.15.5 - - deeplabcut diff --git a/conda-environments/DLC-GPU.yaml b/conda-environments/DLC-GPU.yaml deleted file mode 100644 index fe609dfe54..0000000000 --- a/conda-environments/DLC-GPU.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# DLC-GPU.yaml - -#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 -# -# DeepLabCut environment -# FIRST: INSTALL CORRECT DRIVER for GPU, see https://stackoverflow.com/questions/30820513/what-is-the-correct-version-of-cuda-for-my-nvidia-driver/30820690 -# -# install: conda env create -f DLC-GPU.yaml -# update: conda env update -f DLC-GPU.yaml -name: DLC-GPU -channels: - - conda-forge - - defaults -dependencies: - - python=3.7 - - pip - - cudnn=7 - - jupyter - - nb_conda - - Shapely - - pip: - - tensorflow-gpu==1.15.5 - - deeplabcut[gui] diff --git a/conda-environments/README.md b/conda-environments/README.md index 9a182095a6..e7ff0fee0f 100644 --- a/conda-environments/README.md +++ b/conda-environments/README.md @@ -1 +1 @@ -### Please head over to [Installation](/docs/installation.md) to see how to utilize our supplied conda envs! +# Please head over to [Installation](/docs/installation.md) to see how to utilize our supplied conda envs! diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index 7307e01f30..df55aec39b 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -1,121 +1,277 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# -Please see AUTHORS for contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -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 -# Suppress tensorflow warning messages -import tensorflow as tf - -vers = (tf.__version__).split(".") -if int(vers[0]) == 1 and int(vers[1]) > 12: - TF = tf.compat.v1 # behaves differently before 1.13 -else: - TF = tf - -TF.logging.set_verbosity(TF.logging.ERROR) -DEBUG = True and "DEBUG" in os.environ and os.environ["DEBUG"] -from deeplabcut import DEBUG - -# DLClight version does not support GUIs. Importing accordingly -import matplotlib as mpl - -try: - import wx - - mpl.use("WxAgg") - from deeplabcut import generate_training_dataset - from deeplabcut import refine_training_dataset - from deeplabcut.generate_training_dataset import ( - dropannotationfileentriesduetodeletedimages, - comparevideolistsanddatafolders, - dropimagesduetolackofannotation, - adddatasetstovideolistandviceversa, - dropduplicatesinannotatinfiles, - ) - from deeplabcut.gui import select_crop_parameters - from deeplabcut.gui.launch_script import launch_dlc - from deeplabcut.gui.label_frames import label_frames - from deeplabcut.gui.refine_labels import refine_labels - from deeplabcut.gui.tracklet_toolbox import refine_tracklets - - from deeplabcut.utils.skeleton import SkeletonBuilder -except ModuleNotFoundError: - print( - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)" - ) - mpl.use( - "AGG" - ) # anti-grain geometry engine #https://matplotlib.org/faq/usage_faq.html - - -from deeplabcut.create_project import ( +logger = logging.getLogger(__name__) + +# DEBUG="", "0", "false", "no" -> False +DEBUG = os.environ.get("DEBUG", "").strip().lower() not in {"", "0", "false", "no"} + +from .version import VERSION, __version__ + +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, - 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 ( +from .generate_training_dataset import ( + 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, ) -from deeplabcut.generate_training_dataset import ( - create_training_model_comparison, - create_multianimaltraining_dataset, - cropimagesandlabels, +from .modelzoo.video_inference import video_inference_superanimal +from .pose_estimation_3d import ( + calibrate_cameras, + check_undistortion, + create_labeled_video_3d, + triangulate, ) -from deeplabcut.utils import ( - create_labeled_video, - create_video_with_all_detections, - plot_trajectories, +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, auxiliaryfunctions, convert2_maDLC, convertcsv2h5, - convertannotationdata_fromwindows2unixstyle, - analyze_videos_converth5_to_csv, - auxfun_videos, + create_labeled_video, + create_video_with_all_detections, + plot_trajectories, +) +from .utils.auxfun_videos import ( + CropVideo, + DownSampleVideo, + ShortenVideo, + check_video_integrity, + collect_video_paths, ) -from deeplabcut.utils.auxfun_videos import ShortenVideo, DownSampleVideo, CropVideo +# ----------------------------------------------------------------------------- +# 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"), +} -# Train, evaluate & predict functions / all require TF -from deeplabcut.pose_estimation_tensorflow import ( - train_network, - return_train_network_path, - evaluate_network, - return_evaluate_network_data, - analyze_videos, - analyze_time_lapse_frames, - convert_detections2tracklets, - extract_maps, - visualize_scoremaps, - visualize_locrefs, - visualize_paf, - extract_save_all_maps, - export_model, -) -from deeplabcut.pose_estimation_3d import ( - calibrate_cameras, - check_undistortion, - triangulate, - create_labeled_video_3d, -) +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__)) + -from deeplabcut.refine_training_dataset.stitch import stitch_tracklets -from deeplabcut.refine_training_dataset import extract_outlier_frames, merge_datasets -from deeplabcut.post_processing import filterpredictions, analyzeskeleton +# ----------------------------------------------------------------------------- +# Public API +# ----------------------------------------------------------------------------- +_VERSION_EXPORTS = [ + "__version__", + "VERSION", + "DEBUG", +] -from deeplabcut.version import __version__, VERSION +_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/deeplabcut/__main__.py b/deeplabcut/__main__.py index e11ec38262..ea7e3ca1c6 100644 --- a/deeplabcut/__main__.py +++ b/deeplabcut/__main__.py @@ -1,26 +1,36 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from importlib import import_module -Please see AUTHORS for contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" -try: - import wx - lite = False -except ModuleNotFoundError: - lite = True +def main(): + try: + import_module("PySide6") -# if module is executed directly (i.e. `python -m deeplabcut.__init__`) launch straight into the GUI -if not lite: - print("Starting GUI...") - import deeplabcut + lite = False + except ModuleNotFoundError: + lite = True - deeplabcut.launch_dlc() -else: - print( - "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install deeplabcut[gui]" - ) + # if module is executed directly (i.e. `python -m deeplabcut.__init__`) launch straight into the GUI + if not lite: + print("Starting GUI...") + from deeplabcut.gui.launch_script import launch_dlc + + launch_dlc() + else: + print( + "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install" + "'deeplabcut[gui]''" + ) + + +if __name__ == "__main__": + main() diff --git a/deeplabcut/benchmark/__init__.py b/deeplabcut/benchmark/__init__.py new file mode 100644 index 0000000000..ab863dec97 --- /dev/null +++ b/deeplabcut/benchmark/__init__.py @@ -0,0 +1,123 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 json +import os +from collections.abc import Container +from pathlib import Path +from typing import Literal + +from deeplabcut.benchmark.base import Benchmark, Result, ResultCollection + +DATA_ROOT = Path.cwd() / "data" +CACHE = Path.cwd() / ".results" + +__registry = [] + + +def register(cls): + """Add a benchmark to the list of evaluations to run. + + Apply this function as a decorator to a class. Note that the + class needs to be a subclass of the ``benchmark.base.Benchmark`` + base class. + + In most situations, it will be a subclass of one of the pre-defined + benchmarks in ``benchmark.benchmarks``. + + Throws: + ``ValueError`` if the decorator is applied to a class that is + not a subclass of ``benchmark.base.Benchmark``. + """ + if not issubclass(cls, Benchmark): + raise ValueError(f"Can only register subclasses of {type(Benchmark)}, but got {cls}.") + __registry.append(cls) + + +def evaluate( + include_benchmarks: Container[str] = None, + results: ResultCollection = None, + on_error="return", +) -> ResultCollection: + """Run evaluation for all benchmarks and methods. + + Note that in order for your custom benchmark to be included during + evaluation, the following conditions need to be met: + + - The benchmark subclassed one of the benchmark definitions in + in ``benchmark.benchmarks`` + - The benchmark is registered by applying the ``@benchmark.register`` + decorator to the class + - The benchmark was imported. This is done automatically for all + benchmarks that are defined in submodules or subpackages of the + ``benchmark.submissions`` module. For all other locations, make + sure to manually import the packages **before** calling the + ``evaluate()`` function. + + Args: + include_benchmarks: + If ``None``, run all benchmarks that were discovered. If a container + is passed, only include methods that were defined on benchmarks with + the specified names. E.g., ``include_benchmarks = ["trimouse"]`` would + only evaluate methods of the trimouse benchmark dataset. + on_error: + see documentation in ``benchmark.base.Benchmark.evaluate()`` + + Returns: + A collection of all results, which can be printed or exported to + ``pd.DataFrame`` or ``json`` file formats. + """ + if results is None: + results = ResultCollection() + for benchmark_cls in __registry: + if include_benchmarks is not None: + if benchmark_cls.name not in include_benchmarks: + continue + benchmark = benchmark_cls() + for name in benchmark.names(): + 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) + results.add(result) + return results + + +def get_filepath(basename: str): + return DATA_ROOT / basename + + +def savecache(results: ResultCollection): + with Path(CACHE).open("w") as fh: + json.dump(results.todicts(), fh, indent=2) + + +def loadcache(cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore") -> ResultCollection: + if not Path(cache).exists(): + if on_missing == "raise": + raise FileNotFoundError(cache) + return ResultCollection() + with Path(cache).open() as fh: + try: + data = json.load(fh) + except json.decoder.JSONDecodeError as e: + if on_missing == "raise": + raise e + return ResultCollection() + return ResultCollection.fromdicts(data) diff --git a/deeplabcut/benchmark/__main__.py b/deeplabcut/benchmark/__main__.py new file mode 100644 index 0000000000..28aa6776fb --- /dev/null +++ b/deeplabcut/benchmark/__main__.py @@ -0,0 +1,15 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from deeplabcut.benchmark.cli import main + +if __name__ == "__main__": + main() diff --git a/deeplabcut/benchmark/base.py b/deeplabcut/benchmark/base.py new file mode 100644 index 0000000000..3c9193eb75 --- /dev/null +++ b/deeplabcut/benchmark/base.py @@ -0,0 +1,225 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""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. +Right now, the metrics to compute and report for each of the multi-animal benchmarks is the +root mean-squared-error (RMSE) and the mean average precision (mAP). + +Note for contributors: If you decide to contribute a benchmark which does not fit +into this evaluation framework, please feel free to extend the base classes +(e.g. to support additional metrics). +""" + +import abc +import dataclasses +import warnings +from collections.abc import Iterable + +import pandas as pd + +import deeplabcut.benchmark.metrics +from deeplabcut import __version__ + + +class BenchmarkEvaluationError(RuntimeError): + pass + + +class Benchmark(abc.ABC): + """Abstract benchmark baseclass. + + All benchmarks should subclass this class. + """ + + @abc.abstractmethod + 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. + """ + raise NotImplementedError() + + @abc.abstractmethod + def get_predictions(self): + """Return predictions for all images in the benchmark.""" + raise NotImplementedError() + + 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 to define the {key} property.") + + def compute_pose_rmse(self, results_objects): + return deeplabcut.benchmark.metrics.calc_rmse_from_obj( + results_objects, h5_file=self.ground_truth, metadata_file=self.metadata + ) + + def compute_pose_map(self, results_objects): + return deeplabcut.benchmark.metrics.calc_map_from_obj( + results_objects, h5_file=self.ground_truth, metadata_file=self.metadata + ) + + 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()}") + if on_error not in ("ignore", "return", "raise"): + raise ValueError(f"on_error got an undefined value: {on_error}") + mean_avg_precision = float("nan") + root_mean_squared_error = float("nan") + try: + predictions = self.get_predictions(name) + predictions = self._validate_predictions(name, predictions) + mean_avg_precision = self.compute_pose_map(predictions) + root_mean_squared_error = self.compute_pose_rmse(predictions) + except Exception as exception: + if on_error == "ignore": + # ignore the exception and continue with the next evaluation, without + # yielding a result value. + return + elif on_error == "return": + # return the result value, with NaN as the result for all metrics that + # could not be computed due to the error. + pass + elif on_error == "raise": + # raise the error and stop evaluation + raise BenchmarkEvaluationError(f"Error during benchmark evaluation for model {name}") from exception + else: + raise NotImplementedError() from exception + return Result( + code=self.code, + method_name=name, + benchmark_name=self.name, + mean_avg_precision=mean_avg_precision, + root_mean_squared_error=root_mean_squared_error, + ) + + 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. + """ + 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( + 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.", + stacklevel=2, + ) + + return {img: predictions.get(img, tuple()) for img in test_images} + + +@dataclasses.dataclass +class Result: + """Benchmark result.""" + + code: str + method_name: str + benchmark_name: str + root_mean_squared_error: float = float("nan") + mean_avg_precision: float = float("nan") + benchmark_version: str = __version__ + + _export_mapping = dict( + code="code", + benchmark_name="benchmark", + method_name="method", + benchmark_version="version", + root_mean_squared_error="RMSE", + mean_avg_precision="mAP", + ) + + _primary_key = ("benchmark_name", "method_name", "benchmark_version") + + @property + 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]: + """Names of the primary keys.""" + return tuple(self._export_mapping.get(k) for k in self._primary_key) + + def __str__(self): + return ( + f"{self.method_name}, {self.benchmark_name}: " + f"{self.mean_avg_precision} mAP, " + f"{self.root_mean_squared_error} RMSE" + ) + + @classmethod + def fromdict(cls, data: dict): + """Construct result object from dictionary.""" + kwargs = {attr: data[key] for attr, key in cls._export_mapping.items()} + return cls(**kwargs) + + def todict(self) -> dict: + """Export result object to dictionary, with less verbose key names.""" + return {key: getattr(self, attr) for attr, key in self._export_mapping.items()} + + +class ResultCollection: + def __init__(self, *results): + self.results = {result.primary_key: result for result in results} + + @property + def primary_key_names(self): + return next(iter(self.results.values())).primary_key_names + + 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) + ) + + def add(self, result: Result): + """Add a result to the collection.""" + if result.primary_key in self.results: + raise ValueError( + "An entry for {result.primary_key} does already " + "exist in this collection. Did you try to add the " + "same result twice?" + ) + if len(self) > 0: + if result.primary_key_names != self.primary_key_names: + raise ValueError("Incompatible result format.") + self.results[result.primary_key] = result + + @classmethod + def fromdicts(cls, data: Iterable[dict]): + return cls(*[Result.fromdict(entry) for entry in data]) + + def todicts(self): + return [result.todict() for result in self.results.values()] + + def __len__(self): + return len(self.results) + + def __contains__(self, other: Result): + if not isinstance(other, Result): + 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): + if not isinstance(other, ResultCollection): + return False + return other.results == self.results diff --git a/deeplabcut/benchmark/benchmarks.py b/deeplabcut/benchmark/benchmarks.py new file mode 100644 index 0000000000..0701714a4a --- /dev/null +++ b/deeplabcut/benchmark/benchmarks.py @@ -0,0 +1,214 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""Definition for official DeepLabCut benchmark tasks. + +See benchmark.deeplabcut.org for a current leaderboard with models and metrics +for each of these benchmarks. Submissions can be done by opening a PR in the +benchmark reporistory: + +https://github.com/DeepLabCut/benchmark +""" + +import deeplabcut.benchmark.base + + +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. + """ + + name = "trimouse" + keypoints = ( + "snout", + "leftear", + "rightear", + "shoulder", + "spine1", + "spine2", + "spine3", + "spine4", + "tailbase", + "tail1", + "tail2", + "tailend", + ) + ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Daniel.h5") + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-MultiMouse_70shuffle1.pickle") + num_animals = 3 + + +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. + """ + + name = "parenting" + keypoints = ( + "end1", + "interm1", + "interm2", + "interm3", + "end2", + "snout", + "leftear", + "rightear", + "shoulder", + "spine1", + "spine2", + "spine3", + "spine4", + "tailbase", + "tail1", + "tail2", + "tailend", + ) + + ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mostafizur.h5") + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-CrackingParenting_70shuffle1.pickle") + num_animals = 2 + + def compute_pose_map(self, results_objects): + return deeplabcut.benchmark.metrics.calc_map_from_obj( + results_objects, + h5_file=self.ground_truth, + metadata_file=self.metadata, + oks_sigma=0.15, + margin=10, + symmetric_kpts=[(0, 4), (1, 3)], + ) + + 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()}, + ) + + +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. + """ + + name = "marmosets" + keypoints = ( + "Front", + "Right", + "Middle", + "Left", + "FL1", + "BL1", + "FR1", + "BR1", + "BL2", + "BR2", + "FL2", + "FR2", + "Body1", + "Body2", + "Body3", + ) + ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mackenzie.h5") + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-Marmoset_70shuffle1.pickle") + num_animals = 2 + + +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. + """ + + 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") + num_animals = 14 + + def compute_pose_rmse(self, results_objects): + return deeplabcut.benchmark.metrics.calc_rmse_from_obj( + results_objects, + h5_file=self.ground_truth, + metadata_file=self.metadata, + drop_kpts=[4, 5], + ) + + def compute_pose_map(self, results_objects): + return deeplabcut.benchmark.metrics.calc_map_from_obj( + results_objects, + h5_file=self.ground_truth, + metadata_file=self.metadata, + drop_kpts=[4, 5], + ) diff --git a/deeplabcut/benchmark/cli.py b/deeplabcut/benchmark/cli.py new file mode 100644 index 0000000000..c79146ef2f --- /dev/null +++ b/deeplabcut/benchmark/cli.py @@ -0,0 +1,49 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""Command line interface for DeepLabCut deeplabcut.benchmark.""" + +import argparse + +import deeplabcut.benchmark + + +def _parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--include", nargs="+", default=None, required=False) + parser.add_argument( + "--onerror", + default="return", + required=False, + choices=("ignore", "return", "raise"), + ) + parser.add_argument("--nocache", action="store_true") + return parser.parse_args() + + +def main(): + """Main CLI entry point for generating deeplabcut.benchmark results.""" + args = _parse_args() + if not args.nocache: + results = deeplabcut.benchmark.loadcache() + else: + results = None + results = deeplabcut.benchmark.evaluate( + include_benchmarks=args.include, + results=results, + on_error=args.onerror, + ) + if not args.nocache: + deeplabcut.benchmark.savecache(results) + try: + print(results.toframe()) + except StopIteration: + pass diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py new file mode 100644 index 0000000000..7faf598c89 --- /dev/null +++ b/deeplabcut/benchmark/metrics.py @@ -0,0 +1,257 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""Evaluation metrics for the DeepLabCut benchmark.""" + +import pickle +from collections import defaultdict +from pathlib import Path + +import numpy as np +import pandas as pd + +import deeplabcut.benchmark.utils +from deeplabcut.core import crossvalutils, inferenceutils +from deeplabcut.utils.conversioncode import guarantee_multiindex_rows + + +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") + 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")) + except KeyError: + n_unique = 0 + guarantee_multiindex_rows(df) + file_paths = [Path(*row) for row in df.index.to_list()] + temp = ( + df.stack("individuals", dropna=False) + .reindex(animals, level="individuals") + .reindex(kpts, level="bodyparts", axis=1) + ) + data = temp.to_numpy().reshape((len(file_paths), len(animals), -1, 2)) + if test_indices is not None: + file_paths = [file_paths[i] for i in test_indices] + data = [data[i] for i in test_indices] + + meta = {"animals": animals, "keypoints": kpts, "n_unique": n_unique} + return { + "annotations": dict(zip(file_paths, data, strict=False)), + "metadata": meta, + } + + +def _get_unique_level_values(header, level): + return header.get_level_values(level).unique().to_list() + + +def calc_prediction_errors(preds, gt): + kpts_gt = gt["metadata"]["keypoints"] + kpts_pred = preds["metadata"]["keypoints"] + map_ = {kpts_gt.index(kpt): i for i, kpt in enumerate(kpts_pred)} + annot = gt["annotations"] + + map_images = _map(list(preds["predictions"]), list(annot)) + + errors = np.full( + ( + len(preds["predictions"]), + len(gt["metadata"]["animals"]), + len(kpts_gt), + 2, # Hold distance to GT and confidence + ), + np.nan, + ) + for n, (path, preds_) in enumerate(preds["predictions"].items()): + if not preds_: + continue + xy_gt = annot[map_images[path]].swapaxes(0, 1) + xy_pred = preds_["coordinates"][0] + conf_pred = preds_["confidence"] + for i, xy_gt_ in enumerate(xy_gt): + visible = np.flatnonzero(np.all(~np.isnan(xy_gt_), axis=1)) + xy_pred_ = xy_pred[map_[i]] + 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) + found = neighbors != -1 + if ~np.any(found): + continue + min_dists = np.linalg.norm( + xy_gt_[visible][found] - xy_pred_[neighbors[found]], + axis=1, + ) + conf_pred_ = conf_pred[map_[i]] + errors[n, visible[found], i, 0] = min_dists + errors[n, visible[found], i, 1] = conf_pred_[neighbors[found], 0] + return errors + + +def _map(paths: list[Path], subpaths: list[Path]) -> dict[Path, 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 = {} + paths_ = paths.copy() + subpaths_ = subpaths.copy() + while paths_: + path = paths_.pop() + for s in subpaths_: + if path.parts[-len(s.parts) :] == s.parts: + lookup[path] = s + subpaths_.remove(s) + break + return lookup + + +def conv_obj_to_assemblies(eval_results_obj, keypoint_names): + """Convert predictions to deeplabcut assemblies.""" + assemblies = {} + for image_path, results in eval_results_obj.items(): + lst = [] + for dict_ in results: + ass = inferenceutils.Assembly(len(keypoint_names)) + for i, kpt in enumerate(keypoint_names): + xy = dict_["pose"][kpt] + if ~np.isnan(xy).all(): + joint = inferenceutils.Joint(pos=(xy), label=i) + ass.add_joint(joint) + # TODO(jeylau) add affinity.setter to Assembly + ass._affinity = dict_["score"] + ass._links = [None] + if len(ass): + lst.append(ass) + assemblies[image_path] = lst + return assemblies + + +def calc_map_from_obj( + eval_results_obj, + h5_file, + metadata_file, + oks_sigma=0.1, + margin=0, + symmetric_kpts=None, + drop_kpts=None, +): + """Calculate mean average precision (mAP) based on predictions.""" + eval_results = {Path(k): v for k, v in eval_results_obj.items()} + df = pd.read_hdf(h5_file) + try: + df.drop("single", level="individuals", axis=1, inplace=True) + except KeyError: + pass + n_animals = len(df.columns.get_level_values("individuals").unique()) + kpts = list(df.columns.get_level_values("bodyparts").unique()) + + test_indices = _load_test_indices(metadata_file) + df_test = df.iloc[test_indices] + test_images = load_test_images(h5_file, metadata_file) + missing_images = set(test_images) - set(eval_results) + if len(missing_images) > 0: + raise ValueError( + 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() + } + + # TODO(stes): remove/rewrite + if drop_kpts is not None: + temp = {} + for k, v in assemblies_gt_test.items(): + lst = [] + for a in v: + arr = np.delete(a.data[:, :3], drop_kpts, axis=0) + a = inferenceutils.Assembly.from_array(arr) + lst.append(a) + temp[k] = lst + assemblies_gt_test = temp + for ind in sorted(drop_kpts, reverse=True): + kpts.pop(ind) + + assemblies_pred = conv_obj_to_assemblies(eval_results, kpts) + with deeplabcut.benchmark.utils.DisableOutput(): + oks = inferenceutils.evaluate_assembly( + assemblies_pred, + assemblies_gt_test, + oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + greedy_matching=True, + ) + return oks["mAP"] + + +def calc_rmse_from_obj( + eval_results_obj, + h5_file, + metadata_file, + drop_kpts=None, +): + """Calc prediction errors for submissions.""" + test_indices = _load_test_indices(metadata_file) + gt = _format_gt_data(h5_file, test_indices=test_indices) + kpts = gt["metadata"]["keypoints"] + if drop_kpts: + for k, v in gt["annotations"].items(): + gt["annotations"][k] = np.delete(v, drop_kpts, axis=1) + for ind in sorted(drop_kpts, reverse=True): + kpts.pop(ind) + + test_objects = {Path(k): v for k, v in eval_results_obj.items() if Path(k) in gt["annotations"]} + 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] + raise ValueError( + "Failed to compute the test RMSE: there are test images missing from the" + f"prediction object: {missing_images}" + ) + + assemblies_pred = conv_obj_to_assemblies(test_objects, kpts) + preds = defaultdict(dict) + preds["metadata"]["keypoints"] = kpts + for image, assemblies in assemblies_pred.items(): + if assemblies: + arr = np.stack([a.data for a in assemblies]).swapaxes(0, 1) + data = [xy[~np.isnan(xy).any(axis=1)] for xy in arr[..., :2]] + temp = { + "coordinates": tuple([data]), + "confidence": list(np.expand_dims(arr[..., 2], axis=2)), + } + preds["predictions"][image] = temp + with deeplabcut.benchmark.utils.DisableOutput(): + errors = calc_prediction_errors(preds, gt) + return np.nanmean(errors[..., 0]) + + +def load_test_images(h5file: str, metadata: str) -> list[Path]: + """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] + return [Path(img_path) if isinstance(img_path, str) else Path(*img_path) for img_path in df_test.index] + + +def _load_test_indices(shuffle_metadata_path: str) -> list[int]: + """Returns the indices of test images in the training dataset dataframe.""" + with Path(shuffle_metadata_path).open("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/mot.py b/deeplabcut/benchmark/mot.py new file mode 100644 index 0000000000..1454648d6f --- /dev/null +++ b/deeplabcut/benchmark/mot.py @@ -0,0 +1,200 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from __future__ import annotations + +import warnings + +import motmetrics as mm +import numpy as np +import pandas as pd +from numpy.typing import NDArray + +from deeplabcut.core import trackingutils + + +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. + + Args: + bboxes (numpy.ndarray): A 2D array of shape (N, M), where N is the number of bounding boxes + and M >= 4. The first four columns represent the bounding box in the format + [x_min, y_min, x_max, y_max]. + inplace (bool, optional): If True, modifies the input array in place. If False, returns a copy of + the array with the converted bounding box format. Defaults to False. + + Returns: + numpy.ndarray or None: If `inplace` is False, returns a new array of the same shape as `bboxes` + with the format [x, y, width, height]. If `inplace` is True, the input + array is modified directly, and nothing is returned. + """ + w = bboxes[:, 2] - bboxes[:, 0] + h = bboxes[:, 3] - bboxes[:, 1] + if not inplace: + new_bboxes = bboxes.copy() + new_bboxes[:, 2] = w + new_bboxes[:, 3] = h + return new_bboxes + 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: + """Reconstructs bounding boxes from body part coordinates and likelihoods. + + Args: + data (pandas.DataFrame): A DataFrame containing body part data with a multi-level column index. + The expected levels include 'x', 'y', and 'likelihood', where: + - 'x' and 'y' contain the coordinates of the body parts. + - 'likelihood' contains the confidence scores for each body part. + margin (float): The margin to add/subtract from the minimum/maximum coordinates when defining the bounding box. + to_xywh (bool, optional): If True, converts the bounding box format from [x_min, y_min, x_max, y_max] + to [x, y, width, height]. Defaults to False. + + Returns: + numpy.ndarray: An array of shape (N, 5), where N is the number of rows in `data`. + Each row represents a bounding box with the following values: + - [x_min, y_min, x_max, y_max, likelihood] + If `to_xywh` is True, the format will be [x, y, width, height, likelihood]. + + Note: + - NaN values in the input data are ignored when computing the bounding box dimensions. + - Warnings related to NaN values are suppressed during calculations. + """ + x = data.xs("x", axis=1, level="coords") + y = data.xs("y", axis=1, level="coords") + p = data.xs("likelihood", axis=1, level="coords") + xy = np.stack([x, y], axis=2) + bboxes = np.full((data.shape[0], 5), np.nan) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + bboxes[:, :2] = np.nanmin(xy, axis=1) - margin + bboxes[:, 2:4] = np.nanmax(xy, axis=1) + margin + bboxes[:, 4] = np.nanmean(p, axis=1) + if to_xywh: + convert_bboxes_to_xywh(bboxes, inplace=True) + return bboxes + + +def reconstruct_all_bboxes(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: + """Reconstructs bounding boxes for multiple individuals from body part data. + + Args: + data (pandas.DataFrame): A DataFrame containing body part data with a multi-level column index. + The expected levels include: + - 'individuals': Names of the individuals (e.g., animals). + - 'x', 'y', and 'likelihood': Coordinate and confidence data for body parts. + margin (float): The margin to add/subtract from the minimum/maximum coordinates when defining the bounding box. + to_xywh (bool, optional): If True, converts the bounding box format from [x_min, y_min, x_max, y_max] + to [x, y, width, height]. Defaults to False. + + Returns: + numpy.ndarray: A 3D array of shape (A, F, 5), where: + - A is the number of individuals (excluding 'single', if present). + - F is the number of frames (rows) in the input `data`. + - Each bounding box is represented as [x_min, y_min, x_max, y_max, likelihood]. + If `to_xywh` is True, the format will be [x, y, width, height, likelihood]. + + Note: + - Individuals are extracted from the 'individuals' level of the DataFrame columns. + - If an individual named 'single' exists, it is excluded from the bounding box computation. + - NaN values in the input data are ignored during calculations. + """ + animals = data.columns.get_level_values("individuals").unique().tolist() + try: + animals.remove("single") + except ValueError: + 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) + return bboxes + + +def compute_mot_metrics( + h5_file_gt: str, + h5_file_pred: str, + tracker_type: str = "bbox", + **kwargs, +) -> mm.MOTAccumulator: + df_gt = pd.read_hdf(h5_file_gt) + df = pd.read_hdf(h5_file_pred) + if tracker_type == "bbox": + func = reconstruct_all_bboxes + elif tracker_type == "ellipse": + func = trackingutils.reconstruct_all_ellipses + else: + raise ValueError(f"Unrecognized tracker type {tracker_type}.") + + trackers_gt = func(df_gt, **kwargs) + trackers = func(df, **kwargs) + return _compute_mot_metrics( + trackers_gt, + trackers, + tracker_type, + ) + + +def _compute_mot_metrics( + trackers_ground_truth: NDArray, + trackers: NDArray, + 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`.") + + if tracker_type == "bbox": + sl = slice(0, 4) + cost_func = mm.distances.iou_matrix + elif tracker_type == "ellipse": + sl = slice(0, 5) + + def cost_func(ellipses_gt, ellipses_hyp): + cost_matrix = np.zeros((len(ellipses_gt), len(ellipses_hyp))) + gt_el = [trackingutils.Ellipse(*e[:5]) for e in ellipses_gt] + hyp_el = [trackingutils.Ellipse(*e[:5]) for e in ellipses_hyp] + for i, el in enumerate(gt_el): + for j, tracker in enumerate(hyp_el): + cost_matrix[i, j] = 1 - el.calc_similarity_with(tracker) + return cost_matrix + + else: + raise ValueError(f"Unrecognized tracker type {tracker_type}.") + + ids = np.arange(trackers_ground_truth.shape[0]) + acc = mm.MOTAccumulator(auto_id=True) + for i in range(trackers_ground_truth.shape[1]): + trackers_gt = trackers_ground_truth[:, i, sl] + trackers_hyp = trackers[:, i, sl] + empty_gt = np.isnan(trackers_gt).any(axis=1) + empty_hyp = np.isnan(trackers_hyp).any(axis=1) + trackers_gt = trackers_gt[~empty_gt] + trackers_hyp = trackers_hyp[~empty_hyp] + cost = cost_func(trackers_gt, trackers_hyp) + acc.update(ids[~empty_gt], ids[~empty_hyp], cost) + return acc + + +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) + print(strsummary) + return summary diff --git a/deeplabcut/benchmark/utils.py b/deeplabcut/benchmark/utils.py new file mode 100644 index 0000000000..fc5492ade4 --- /dev/null +++ b/deeplabcut/benchmark/utils.py @@ -0,0 +1,73 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""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 +import os +import pkgutil +import sys +from pathlib import Path + + +class RedirectStdStreams: + """Context manager for redirecting stdout and stderr + Reference: + https://stackoverflow.com/a/6796752 + CC BY-SA 3.0, https://stackoverflow.com/users/46690/rob-cowie + """ + + def __init__(self, stdout=None, stderr=None): + self._stdout = stdout or sys.stdout + self._stderr = stderr or sys.stderr + + def __enter__(self): + self.old_stdout, self.old_stderr = sys.stdout, sys.stderr + self.old_stdout.flush() + self.old_stderr.flush() + sys.stdout, sys.stderr = self._stdout, self._stderr + + def __exit__(self, exc_type, exc_value, traceback): + self._stdout.flush() + self._stderr.flush() + sys.stdout = self.old_stdout + sys.stderr = self.old_stderr + + +class DisableOutput(RedirectStdStreams): + def __init__(self): + devnull = Path(os.devnull).open("w") + super().__init__(stdout=devnull, stderr=devnull) + + +def import_submodules(package, recursive=True): + """Import all submodules of a module, recursively, including subpackages. + + :param package: package (name or actual module) + :type package: str | module + :rtype: dict[str, types.ModuleType] + + Reference: + https://stackoverflow.com/a/25562415 + CC BY-SA 3.0, https://stackoverflow.com/users/712522/mr-b + """ + if isinstance(package, str): + package = importlib.import_module(package) + results = {} + 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: + results.update(import_submodules(full_name)) + return results diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 35f2c08064..298cf1c987 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -1,18 +1,18 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + from pathlib import Path import click -import deeplabcut - CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -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") @@ -49,39 +49,50 @@ 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 - - Options \n - ---------- \n - project : string \n - \tString containing the name of the project.\n - experimenter : string \n - \tString containing the name of the experimenter. \n - 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 - 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 - - 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/ - - 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 - - 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 - + """Create a new project directory, sub-directories and a basic configuration file. + + Delegates to ``deeplabcut.create_new_project``. + + The configuration file is loaded with default values. Change its parameters to your + projects need. + + Args: + project (string): String containing the name of the project. + experimenter (string): String containing the name of the experimenter. + videos (list): A list of string containing the full paths of the videos to include in the project. + 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. + copy_videos (bool, optional): If True, symlink videos into project/videos directory. + The default is ``True``; if provided it must be either ``True`` or ``False``. + + Examples: + To create the project in the current working directory without symbolic links: + ```bash + 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 the current working directory with symbolic links: + ```bash + 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: + + ```bash + 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 new.create_new_project(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -95,32 +106,31 @@ 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. - Options\n - ----------\n - config : string\n - String containing the full path of the config file in the project. + Delegates to ``deeplabcut.add_new_videos``. - videos : list \n - A list of string containing the full paths of the videos to include in the project. + Args: + config (string): String containing the full path of the config file in the project. + videos (list): A list of string containing the full paths of the videos to include in the project. + copy_videos (bool, optional): If True, symlinks of the videos are copied to + project/videos. Default ``True``; must be ``True`` or ``False``. - 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`` - - Examples\n - --------\n - >>> python3 dlc.py add_new_videos /home/project/reaching-task-Tanmay-2018-08-23/config.yaml /data/videos/mouse5.avi + Examples: + To add a new video to the project: + ```bash + 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 add.add_new_videos(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("mode") @@ -139,63 +149,95 @@ 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_video`` 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 - MODE : string \n \n - String containing the mode of extraction. It must be either ``automatic`` or ``manual``. \n - - Examples \n - -------- \n - 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 - >>> python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml automatic --crop - -------- \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 - -------- \n + """Extracts frames from the videos in the config.yaml file. + + Delegates to ``deeplabcut.extract_frames``. + + Only the videos in the config.yaml will be used to select the frames. Use the + function ``add_new_videos`` at any stage of the project to add new videos to the + config file and extract their frames. + + Args: + config (string): Full path of the config.yaml file as a string. + mode (string): Mode of extraction. Must be either ``automatic`` or ``manual``. + algo (string, optional): For automatic extraction, the algorithm to use: + ``kmeans`` or ``uniform``. Defaults to ``uniform``. + crop (bool, optional): If True, crop frames according to config.yaml parameters. + Defaults to False. + + Examples: + For selecting frames automatically with 'kmeans' and do not want to crop the frames: + + ```bash + python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml \\ + automatic --algo kmeans + ``` + + For selecting frames automatically with 'uniform' and want to crop the frames based on + the ``crop`` parameters in config.yaml: + + ```bash + python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml \\ + automatic --crop + ``` + + To select frames manually: + + ```bash + python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml manual + ``` + 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. """ - 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) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @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 - Example\n - --------\n - python3 dlc.py label_frames /analysis/project/reaching-task/config.yaml + """Manually label/annotate the extracted frames. + + Delegates to ``deeplabcut.label_frames``. + + Update the list of body parts you want to localize in the config.yaml file first. + + Args: + config (string): Full path of the config.yaml file. + + Examples: + To launch the frame labeling GUI: + ```bash + 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) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @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. + Delegates to ``deeplabcut.check_labels``. + + If some are wrong, then use the refine_labels to correct the labels. """ - 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) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -207,23 +249,42 @@ 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 - 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 - --------\n - To create a training dataset with only 1 shuffle - python3 dlc.py create_training_dataset /analysis/project/reaching-task/config.yaml - - To create a training dataset with only 2 shuffles - python3 dlc.py create_training_dataset /analysis/project/reaching-task/config.yaml num_shuffles 2 + """Combine frame and label information into an array. Create training and test sets. + + Delegates to ``deeplabcut.create_training_dataset``. + + Update parameters TrainFraction and iteration in config.yaml. Also update + parameters for pose_config.yaml as wanted. + + Args: + config (string): Full path of the config.yaml file in the train directory of a + project. + num_shuffles (int, optional): Number of shuffles of training dataset to create. + Defaults to 1. + + Examples: + To create a training dataset with only 1 shuffle: + + ```bash + python3 dlc.py create_training_dataset \\ + /analysis/project/reaching-task/config.yaml + ``` + + To create a training dataset with only 2 shuffles: + + ```bash + 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) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -235,20 +296,28 @@ def create_training_dataset(_, *args, **kwargs): ) @click.pass_context def train_network(_, *args, **kwargs): - """Train a trained Feature detector with a specific training data set.\n - Provide path to the pose_config file. - CONFIG: Full path of the config.yaml file in the train directory of a project.\n + """Train a trained Feature detector with a specific training data set. - e.g. run the script like this: - python3 dlc.py step7_train /home/project/reaching/config.yaml + Delegates to ``deeplabcut.train_network``. + Args: + config (string): Full path of the config.yaml file in the train directory of a + project. + shuffle (int, optional): Shuffle index of the training dataset. Defaults to 1. + + Examples: + To train the network with the default settings: + + ```bash + python3 dlc.py step7_train /home/project/reaching/config.yaml + ``` """ from deeplabcut.pose_estimation_tensorflow import training training.train_network(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -258,31 +327,37 @@ 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 - CONFIG: Full path of the "pose_config.yaml" file in the train directory of a project.\n + """Evaluates a trained Feature detector model. + + Delegates to ``deeplabcut.evaluate_network``. - Example\n - ---------- - Evalaute the network - python3 dlc.py evaluate_network /home/project/reaching/config.yaml + Args: + config (string): Full path of the config.yaml file in the train directory of a + project. + shuffle (list, optional): Shuffle index of the training dataset. Defaults to [1]. + plotting (bool, optional): Make evaluation plots. Defaults to False. + Examples: + Evalaute the network: + + ```bash + 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) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") -@click.argument("video", nargs=-1) +@click.argument("videos", nargs=-1) @click.option( "-num", "--num_shuffles", @@ -293,7 +368,7 @@ def evaluate_network(_, config, **kwargs): @click.option( "-vtype", "--video_type", - "videotype", + "video_extensions", default=".avi", help="The extension of video in case the input is a directory", ) @@ -306,15 +381,26 @@ def evaluate_network(_, config, **kwargs): ) @click.pass_context def analyze_videos(_, *args, **kwargs): - - """Makes prediction.\n - CONFIG: Full path of the "config.yaml" file in the train directory of a project.\n - VIDEOS: Full path to video.\n - - Example\n - ---------- - - python3 dlc.py analyze_videos /home/project/reaching/config.yaml /home/project/reaching/newVideo/1.avi + """Makes prediction on videos using a trained network. + + Delegates to ``deeplabcut.analyze_videos``. + + Args: + config (string): Full path of the config.yaml file in the train directory of a + project. + videos (list): Full path(s) to video(s). + shuffle (int, optional): Shuffle index of the training dataset. Defaults to 1. + video_extensions (string, optional): Video extension when the input is a directory. + Defaults to ``.avi``. + save_as_csv (bool, optional): Also save predictions as a CSV file. Defaults to + False. + + Examples: + To analyze a video: + ```bash + python3 dlc.py analyze_videos /home/project/reaching/config.yaml \\ + /home/project/reaching/newVideo/1.avi + ``` """ from deeplabcut.pose_estimation_tensorflow import predict_videos @@ -325,28 +411,33 @@ def analyze_videos(_, *args, **kwargs): # predict.predict_video(config, video,**kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") -@click.argument("video") +@click.argument("videos") @click.option( "-num", "--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", "--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", @@ -354,8 +445,9 @@ def analyze_videos(_, *args, **kwargs): "comparisonbodyparts", 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.", + 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.", ) @click.option( "-e", @@ -363,15 +455,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", @@ -379,15 +474,15 @@ def analyze_videos(_, *args, **kwargs): "ARdegree", default=7, help="For outlieralgorithm `fitting`: Autoregressive degree of Sarimax model degree. \ - See https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html", + See https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html", ) @click.option( "-mad", "--ma_degree", "MAdegree", default=1, - help="Int value. For outlieralgorithm `fitting`: MovingAvarage degree of Sarimax model degree.\ - See https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html", + help="Int value. For outlieralgorithm `fitting`: Moving Average degree of Sarimax model degree.\ + See https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html", ) @click.option( "-a", @@ -401,64 +496,106 @@ 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): - """ - 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'. - - CONFIG : string \n - Full path of the config.yaml file as a string. \n - VIDEO : Full path of the video to extract the frame from. Make sure that this video is already analyzed. - - - 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 - --------\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 - --------\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 - --------\n - + """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. + + Delegates to ``deeplabcut.extract_outlier_frames``. + + Another crucial parameter in config.yaml is how many frames to extract + 'numframes2extract'. + + Args: + config (string): Full path of the config.yaml file as a string. + video (string): Full path of the video to extract frames from. Make sure that + this video is already analyzed. + outlieralgorithm (string, optional): Algorithm used to detect outliers. + Defaults to ``fitting``. + comparisonbodyparts (string, optional): Body parts used for comparison. + Defaults to ``all``. + epsilon (float, optional): Meaning depends on outlier algorithm. Defaults to 20. + p_bound (float, optional): Likelihood threshold for ``uncertain`` algorithm. + Defaults to 0.01. + ARdegree (int, optional): Autoregressive degree for ``fitting`` algorithm. + Defaults to 7. + MAdegree (int, optional): Moving average degree for ``fitting`` algorithm. + Defaults to 1. + alpha (float, optional): Significance level for SARIMAX outlier detection. + Defaults to 0.01. + extractionalgorithm (string, optional): Algorithm for selecting outlier frames. + Defaults to ``uniform``. + + Examples: + For extracting the frames with default settings: + + ```bash + python3 dlc.py extract_outlier_frames \\ + /analysis/project/reaching-task/config.yaml \\ + /analysis/project/video/reachinvideo1.avi + ``` + + For extracting the frames with kmeans: + + ```bash + python3 dlc.py extract_outlier_frames \\ + /analysis/project/reaching-task/config.yaml \\ + /analysis/project/video/reachinvideo1.avi \\ + --extractionalgorithm 'kmeans' + ``` + + For extracting the frames with kmeans and epsilon = 5 pixels: + + ```bash + python3 dlc.py extract_outlier_frames \\ + /analysis/project/reaching-task/config.yaml \\ + /analysis/project/video/reachinvideo1.avi \\ + --epsilon 5 --extractionalgorithm kmeans + ``` """ from deeplabcut.refine_training_dataset import outlier_frames outlier_frames.extract_outlier_frames(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @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 - - Examples \n - --------\n - >>> python3 dlc.py refine_labels /analysis/project/reaching-task/config.yaml \n - --------\n + """Refines the labels of the outlier frames extracted from the analyzed videos. + + Delegates to ``deeplabcut.refine_labels``. + + Helps in augmenting the training dataset. Use the function ``analyze_videos`` to + analyze a video and extract the outlier frames using ``extract_outlier_frames`` + before refining the labels. + + Args: + config (string): Full path of the config.yaml file. + + Examples: + To refine the labels: + ```bash + python3 dlc.py refine_labels /analysis/project/reaching-task/config.yaml + ``` """ from deeplabcut.refine_training_dataset import outlier_frames outlier_frames.refine_labels(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") -@click.argument("video", nargs=-1) +@click.argument("videos", nargs=-1) @click.option( "-num", "--num_shuffles", @@ -469,10 +606,10 @@ def refine_labels(_, config): @click.option( "-v", "--video_type", - "videotype", + "video_extensions", default=".avi", help="Checks for the extension of the video in case the input is a directory.\ - Only videos with this extension are analyzed. The default is ``.avi``", + Only videos with this extension are analyzed. The default is ``.avi``", ) @click.option( "-s", @@ -481,9 +618,9 @@ def refine_labels(_, config): is_flag=True, default=False, help="If true creates each frame individual and then combines into a video. \ - This variant is relatively slow as it stores all individual frames. However, it \ - uses matplotlib to create the frames and is therefore much more flexible \ - (one can set transparency of markers, crop, and easily customize.", + This variant is relatively slow as it stores all individual frames. However, it \ + uses matplotlib to create the frames and is therefore much more flexible \ + (one can set transparency of markers, crop, and easily customize.", ) @click.option( "-d", @@ -492,22 +629,36 @@ def refine_labels(_, config): is_flag=True, default=False, help="If true then the individual frames created during the video generation will be deleted.\ - Only the video will be left.", + Only the video will be left.", ) @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. + + Delegates to ``deeplabcut.create_labeled_video``. + + Make sure the video is already analyzed by the function ``analyze_videos``. + + Args: + config (string): Full path of the config.yaml file. + videos (list): Full path(s) to video(s). + shuffle (int, optional): Shuffle index of the training dataset. Defaults to 1. + video_extensions (string, optional): Video extension when the input is a directory. + Defaults to ``.avi``. + save_frames (bool, optional): Save individual frames before combining into video. + Defaults to False. + delete (bool, optional): Delete individual frames after video generation. + Defaults to False. """ from deeplabcut.utils import make_labeled_video make_labeled_video.create_labeled_video(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") -@click.argument("video", nargs=-1) +@click.argument("videos", nargs=-1) @click.option( "-num", "--num_shuffles", @@ -518,10 +669,10 @@ def create_labeled_video(_, *args, **kwargs): @click.option( "-v", "--video_type", - "videotype", + "video_extensions", default=".avi", help="Checks for the extension of the video in case the input is a directory.\ - Only videos with this extension are analyzed. The default is ``.avi``", + Only videos with this extension are analyzed. The default is ``.avi``", ) @click.option( "-s", @@ -533,21 +684,32 @@ def create_labeled_video(_, *args, **kwargs): ) @click.pass_context def plot_trajectories(_, *args, **kwargs): - """ - Plots the trajectories of various bodyparts across the video.\n - - 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 - --------\n + """Plots the trajectories of various bodyparts across the video. + + Delegates to ``deeplabcut.plot_trajectories``. + + Args: + config (string): Full path of the config.yaml file. + videos (list): Full path(s) to video(s). + shuffle (int, optional): Shuffle index of the training dataset. Defaults to 1. + video_extensions (string, optional): Video extension when the input is a directory. + Defaults to ``.avi``. + showfigures (bool, optional): Also display plots interactively. Defaults to False. + + Examples: + For plotting trajectories: + ```bash + python3 dlc.py plot_trajectories + /analysis/project/reaching-task/config.yaml \\ + /analysis/project/videos/reachingvideo1.avi + ``` """ from deeplabcut.utils import plotting plotting.plot_trajectories(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("cfg-path", nargs=1, type=click.STRING) @click.option( @@ -609,45 +771,33 @@ def plot_trajectories(_, *args, **kwargs): ) @click.pass_context def export_model(_, *args, **kwargs): + """Export DLC models for the model zoo or for live inference. + + Delegates to ``deeplabcut.export_model``. + + Saves the pose configuration, snapshot files, and frozen graph of the model to a + directory named exported-models within the project directory. + + Args: + cfg_path (string): Path to the DLC Project config.yaml file. + iteration (int, optional): The model iteration you wish to export. + If None, uses the iteration listed in the config file. + shuffle (int, optional): The shuffle of the model to export. Defaults to 1. + trainingsetindex (int, optional): Index of the training fraction for the model + to export. Defaults to 0. + snapshotindex (int, optional): The snapshot index for the weights you wish to + export. If None, uses the snapshotindex as defined in config.yaml. + Defaults to None. + TFGPUinference (bool, optional): Use the tensorflow inference model? + Defaults to True. For DeepLabCut-live, set TFGPUinference=False. + overwrite (bool, optional): If the model was already exported, whether to + overwrite. Defaults to False. + make_tar (bool, optional): Compress the exported directory to a tar file? + Defaults to True. Required for model zoo export, not for live inference. """ - 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 - ----------- - - cfg_path : string\n - \tpath to the DLC Project config.yaml file - - iteration : int, optional\n - \tthe model iteration you wish to export.\n - \tIf None, uses the iteration listed in the config file - - shuffle : int, optional\n - \tthe shuffle of the model to export. default = 1 - - trainingsetindex : int, optional\n - \tthe index of the training fraction for the model you wish to export. default = 1 - - snapshotindex : int, optional\n - \tthe snapshot index for the weights you wish to export.\n - \tIf None, uses the snapshotindex as defined in 'config.yaml'. Default = None - - TFGPUinference : bool, optional\n - \tuse the tensorflow inference model? Default = True\n - \tFor inference using DeepLabCut-live, it is recommended to set TFGPIinference=False - - overwrite : bool, optional\n - \tif the model you wish to export has already been exported, whether to overwrite. default = False - - make_tar : bool, optional\n - \tDo you want to compress the exported directory to a tar file? Default = True\n - \tThis is necessary to export to the model zoo, but not for live inference. - """ - from deeplabcut import export_model export_model(*args, **kwargs) -########################################################################################################################### +########################################################################## diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py new file mode 100644 index 0000000000..51e8301208 --- /dev/null +++ b/deeplabcut/compat.py @@ -0,0 +1,1687 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Compatibility file for methods available with either PyTorch or Tensorflow.""" + +from __future__ import annotations + +from collections.abc import Sequence +from pathlib import Path + +import numpy as np + +import deeplabcut.core.visualization as visualization +from deeplabcut.core.config import read_config +from deeplabcut.core.deprecation import renamed_parameter +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset.metadata import get_shuffle_engine + +DEFAULT_ENGINE = Engine.PYTORCH + + +def _coerce_video_paths(videos: list[str | Path]) -> list[Path]: + return [Path(v) for v in videos] + + +def _coerce_optional_path(path: str | Path | None) -> Path | None: + return None if path is None else Path(path) + + +def get_project_engine(cfg: dict) -> Engine: + """Get the project engine. + + Args: + cfg: the project configuration file + + Returns: + the engine specified for the project, or the default engine if none is specified + """ + if cfg.get("engine") is not None: + return Engine(cfg["engine"]) + + return DEFAULT_ENGINE + + +def get_available_aug_methods(engine: Engine) -> tuple[str, ...]: + """Get the available aug methods. + + Args: + engine: the engine for which augmentation methods should be returned + + Returns: + the augmentations available for the given engine, where the first one is the + default method to use + + Raises: + RuntimeError: If no augmentations methods are defined for the given engine + """ + if engine == Engine.TF: + return "imgaug", "default", "deterministic", "scalecrop", "tensorpack" + elif engine == Engine.PYTORCH: + return ("albumentations",) + + raise RuntimeError(f"Unknown augmentation for engine: {engine}") + + +@renamed_parameter(old="maxiters", new="max_iters", since="3.0.0") +@renamed_parameter(old="saveiters", new="save_iters", since="3.0.0") +@renamed_parameter(old="displayiters", new="display_iters", since="3.0.0") +def train_network( + config: str | Path, + shuffle: int = 1, + trainingsetindex: int = 0, + max_snapshots_to_keep: int | None = None, + display_iters: int | None = None, + save_iters: int | None = None, + max_iters: int | None = None, + epochs: int | None = None, + save_epochs: int | None = None, + allow_growth: bool = True, + gputouse: str | None = None, + autotune: bool = False, + keepdeconvweights: bool = True, + modelprefix: str = "", + superanimal_name: str = "", + superanimal_transfer_learning: bool = False, + engine: Engine | None = None, + device: str | None = None, + snapshot_path: str | Path | None = None, + detector_path: str | Path | None = None, + batch_size: int | None = None, + detector_batch_size: int | None = None, + detector_epochs: int | None = None, + detector_save_epochs: int | None = None, + pose_threshold: float | None = 0.1, + pytorch_cfg_updates: dict | None = None, +): + """Trains the network with the labels in the training dataset. + + Args: + config (str | Path): Full path of the config.yaml file. + shuffle (int, optional): Integer value specifying the shuffle index to select for training. Defaults to 1. + trainingsetindex (int, optional): Integer specifying which TrainingsetFraction to use. + Note that TrainingFraction is a list in config.yaml. Defaults to 0. + max_snapshots_to_keep (int or None): Sets how many snapshots are kept, i.e. states of the trained network. Every + saving iteration many times a snapshot is stored, however only the last + ``max_snapshots_to_keep`` many are kept! If you change this to None, then all + are kept. + See: https://github.com/DeepLabCut/DeepLabCut/issues/8#issuecomment-387404835 + display_iters (optional): This variable is actually set in ``pose_config.yaml``. However, you can + overwrite it with this hack. Don't use this regularly, just if you are too lazy + to dig out the ``pose_config.yaml`` file for the corresponding project. If + ``None``, the value from there is used, otherwise it is overwritten! Defaults to None. + save_iters (optional): Only for the TensorFlow engine (for the PyTorch engine see the + ``torch_kwargs``: you can use ``save_epochs``). This variable is actually set in + ``pose_config.yaml``. However, you can overwrite it with this hack. Don't use this regularly, + just if you are too lazy to dig out the ``pose_config.yaml`` file for the corresponding project. + If ``None``, the value from there is used, otherwise it is overwritten! Defaults to None. + max_iters (optional): Only for the TensorFlow engine (for the PyTorch engine see the + ``torch_kwargs``: you can use ``epochs``). This variable is actually set in + ``pose_config.yaml``. However, you can overwrite it with this hack. Don't use this regularly, + just if you are too lazy to dig out the ``pose_config.yaml`` file for the corresponding project. + If ``None``, the value from there is used, otherwise it is overwritten! Defaults to None. + epochs (optional): Only for the PyTorch engine (equivalent to the `max_iters` parameter for the + TensorFlow engine). The maximum number of epochs to train the model for. If None, the value will be read + from the `pytorch_config.yaml` file. 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 size 1 (or 32 iterations with batch size 2, 16 with batch size 4, + etc.). Defaults to None. + save_epochs (optional): Only for the PyTorch engine (equivalent to the `save_iters` parameter for the + TensorFlow engine). The number of epochs between each snapshot save. If None, the value will be read from + the `pytorch_config.yaml` file. Defaults to None. + allow_growth (bool, optional): Only for the TensorFlow engine. For some smaller GPUs the memory issues happen. + If ``True``, the memory allocator does not pre-allocate the entire specified GPU memory region, instead + starting small and growing as needed. + See issue: https://forum.image.sc/t/how-to-stop-running-out-of-vram/30551/2. Defaults to True. + gputouse (optional): Only for the TensorFlow engine (for the PyTorch engine see the ``torch_kwargs``: you can + use ``device``). Natural number indicating the number of your GPU (see number in nvidia-smi). + If you do not have a GPU put None. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + autotune (bool, optional): Only for the TensorFlow engine. Property of TensorFlow, somehow faster if ``False`` + (as Eldar found out, see https://github.com/tensorflow/tensorflow/issues/13317). Defaults to False. + keepdeconvweights (bool, optional): Also restores the weights of the deconvolution layers (and the backbone) + when training from a snapshot. Note that if you change the number of bodyparts, you need to set this to + false for re-training. Defaults to True. + modelprefix (str, optional): Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. Defaults to "". + superanimal_name (str, optional): Only for the TensorFlow engine. For the PyTorch engine, you need to specify + this through the ``weight_init`` when creating the training dataset. Specified if transfer learning with + superanimal is desired. Defaults to "". + superanimal_transfer_learning (bool, optional): Only for the TensorFlow engine. For the PyTorch engine, you + need to specify this through the ``weight_init`` when creating the training dataset. If set true, the + training is transfer learning (new decoding layer). If set false, and superanimal_name is True, then the + training is fine-tuning (reusing the decoding layer). Defaults to False. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. + You can overwrite this by passing the engine as an argument, but this should generally not be done. + Defaults to None. + device (str, optional): Only for the PyTorch engine. The device to run the training on (e.g. "cuda:0"). + Defaults to None. + snapshot_path (str | Path, optional): Only for the PyTorch engine. The path to the pose model snapshot to + resume training from. Defaults to None. + detector_path (str | Path, optional): Only for the PyTorch engine. The path to the detector model snapshot to + resume training from. Defaults to None. + batch_size (int, optional): Only for the PyTorch engine. The batch size to use while training. Defaults to None. + detector_batch_size (int, optional): Only for the PyTorch engine. The batch size to use while training the + detector. Defaults to None. + detector_epochs (int, optional): Only for the PyTorch engine. The number of epochs to train the detector for. + Defaults to None. + detector_save_epochs (int, optional): Only for the PyTorch engine. The number of epochs between each detector + snapshot save. Defaults to None. + pose_threshold (float, optional): Only for the PyTorch engine. Used for memory-replay. Pseudo-predictions with + confidence lower than this threshold are discarded for memory-replay. Defaults to 0.1. + pytorch_cfg_updates (dict, optional): A dictionary of updates to the pytorch config. The keys are the + dot-separated paths to the values to update in the config. For example, to update the gpus to run the + training on, you can use: ``pytorch_cfg_updates = {"runner.gpus": [0, 1, 2, 3]}``. Defaults to None. + + Returns: + None + + Examples: + To train the network for first shuffle of the training dataset: + + deeplabcut.train_network("/analysis/project/reaching-task/config.yaml") + + To train the network for second shuffle of the training dataset: + + deeplabcut.train_network( + '/analysis/project/reaching-task/config.yaml', + shuffle=2, + keepdeconvweights=True, + ) + + To train the network for shuffle created with a PyTorch engine, while overriding the + number of epochs, batch size and other parameters. + + deeplabcut.train_network( + '/analysis/project/reaching-task/config.yaml', + shuffle=1, + batch_size=8, + epochs=100, + save_epochs=10, + display_iters=50, + ) + """ + config = Path(config) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import train_network + + if max_snapshots_to_keep is None: + max_snapshots_to_keep = 5 + + return train_network( + str(config), + shuffle=shuffle, + trainingsetindex=trainingsetindex, + max_snapshots_to_keep=max_snapshots_to_keep, + displayiters=display_iters, + saveiters=save_iters, + maxiters=max_iters, + allow_growth=allow_growth, + gputouse=gputouse, + autotune=autotune, + keepdeconvweights=keepdeconvweights, + superanimal_name=superanimal_name, + superanimal_transfer_learning=superanimal_transfer_learning, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import train_network + + return train_network( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + device=device, + snapshot_path=snapshot_path, + detector_path=detector_path, + load_head_weights=keepdeconvweights, + batch_size=batch_size, + epochs=epochs, + save_epochs=save_epochs, + detector_batch_size=detector_batch_size, + detector_epochs=detector_epochs, + detector_save_epochs=detector_save_epochs, + display_iters=display_iters, + max_snapshots_to_keep=max_snapshots_to_keep, + pose_threshold=pose_threshold, + pytorch_cfg_updates=pytorch_cfg_updates, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def return_train_network_path( + config: str | Path, + shuffle: int = 1, + trainingsetindex: int = 0, + 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. + + Args: + config (str | Path): Full path of the config.yaml file. + shuffle (int): 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). + modelprefix (str, optional): Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Returns: + tuple: trainposeconfigfile, testposeconfigfile, snapshotfolder. + """ + config = Path(config) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import return_train_network_path + + return return_train_network_path( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis.utils import ( + return_train_network_path, + ) + + return return_train_network_path( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +@renamed_parameter(old="Shuffles", new="shuffles", since="3.0.0") +def evaluate_network( + config: str | Path, + shuffles: Sequence[int] = (1,), + trainingsetindex: int | str = 0, + plotting: bool | str = False, + show_errors: bool = True, + comparison_bodyparts: str | list[str] = "all", + gputouse: str | None = None, + rescale: bool = False, + modelprefix: str = "", + per_keypoint_evaluation: bool = False, + snapshots_to_evaluate: list[str] | None = None, + pcutoff: float | list[float] | dict[str, float] | None = None, + engine: Engine | None = None, + **torch_kwargs, +): + """Evaluates the network. + + Evaluates the network based on the saved models at different stages of the training + network. The evaluation results are stored in the .h5 and .csv file under the + subdirectory 'evaluation_results'. Change the snapshotindex parameter in the config + file to 'all' in order to evaluate all the saved models. + + Args: + config (str | Path): Full path of the config.yaml file. + shuffles (Sequence[int], optional): List of integers specifying the shuffle indices of the training dataset. + Defaults to [1]. + trainingsetindex (int or str, optional): Integer specifying which "TrainingsetFraction" to use. Note that + "TrainingFraction" is a list in config.yaml. This variable can also be set to "all". Defaults to 0. + plotting (bool or str, optional): Plots the predictions on the train and test images. If provided it must be + either ``True``, ``False``, ``"bodypart"``, or ``"individual"``. Setting to ``True`` defaults as + ``"bodypart"`` for multi-animal projects. If a detector is used, the predicted bounding boxes will also be + plotted. Defaults to False. + show_errors (bool, optional): Display train and test errors. Defaults to True. + comparison_bodyparts (str or list, optional): The average error will be computed for those body parts only. The + provided list has to be a subset of the defined body parts. Defaults to "all". + gputouse (int or None, optional): Indicates the GPU to use (see number in ``nvidia-smi``). If you do not have a + GPU put `None`. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + rescale (bool, optional): Evaluate the model at the ``'global_scale'`` variable (as set in the + ``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! Defaults to False. + modelprefix (str, optional): Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. Defaults to "". + per_keypoint_evaluation (bool): Compute the train and test RMSE for each keypoint, and save the results to a + {model_name}-keypoint-results.csv in the evaluation-results folder. Defaults to False. + snapshots_to_evaluate (List[str], optional): List of snapshot names to evaluate (e.g. ["snapshot-5000", + "snapshot-7500"]). Defaults to None. + pcutoff (float | list[float] | dict[str, float] | None): Only for the PyTorch engine. For the TensorFlow engine, + please set the pcutoff in the `config.yaml` file. The cutoff to use for computing evaluation metrics. When + `None` (default), the cutoff will be loaded from the project config. If a list is provided, there should be + one value for each bodypart and one value for each unique bodypart (if there are any). If a dict is + provided, the keys should be bodyparts mapping to pcutoff values for each bodypart. Bodyparts that are not + defined in the dict will have pcutoff set to 0.6. Defaults to None. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + torch_kwargs: You can add any keyword arguments for the deeplabcut.pose_estimation_pytorch evaluate_network + function here. These arguments are passed to the downstream function. Available parameters are + `snapshotindex`, which overrides the `snapshotindex` parameter in the project configuration file. For + top-down models the `detector_snapshot_index` parameter can override the index of the detector to use for + evaluation in the project configuration file. + + Returns: + None + + Examples: + If you do not want to plot and evaluate with shuffle set to 1. + + deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', shuffles=[1], + ) + + If you want to plot and evaluate with shuffle set to 0 and 1. + + deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', + shuffles=[0, 1], + plotting=True, + ) + + If you want to plot assemblies for a maDLC project: + + deeplabcut.evaluate_network( + '/analysis/project/reaching-task/config.yaml', + shuffles=[1], + plotting="individual", + ) + + If you have a PyTorch model for which you want to set a different p-cutoff for + "left_ear" and "right_ear" bodyparts, and keep the one set in the project config + for other bodyparts: + + deeplabcut.evaluate_network( + "/analysis/project/reaching-task/config.yaml", + shuffles=[0, 1], + pcutoff={"left_ear": 0.8, "right_ear": 0.8}, + ) + + Note: + This defaults to standard plotting for single-animal projects. + """ + config = Path(config) + if engine is None: + cfg = read_config(config) + engines = set() + for shuffle in shuffles: + engines.add( + get_shuffle_engine( + cfg, + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + ) + if len(engines) == 0: + 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)})") + engine = engines.pop() + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import evaluate_network + + return evaluate_network( + str(config), + Shuffles=shuffles, + trainingsetindex=trainingsetindex, + plotting=plotting, + show_errors=show_errors, + comparisonbodyparts=comparison_bodyparts, + gputouse=gputouse, + rescale=rescale, + modelprefix=modelprefix, + per_keypoint_evaluation=per_keypoint_evaluation, + snapshots_to_evaluate=snapshots_to_evaluate, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import evaluate_network + + _update_device(gputouse, torch_kwargs) + return evaluate_network( + config, + shuffles=shuffles, + trainingsetindex=trainingsetindex, + plotting=plotting, + show_errors=show_errors, + comparison_bodyparts=comparison_bodyparts, + snapshots_to_evaluate=snapshots_to_evaluate, + per_keypoint_evaluation=per_keypoint_evaluation, + modelprefix=modelprefix, + pcutoff=pcutoff, + **torch_kwargs, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +@renamed_parameter(old="Snapindex", new="snapshotindex", since="3.0.0") +def return_evaluate_network_data( + config: str | Path, + shuffle: int = 0, + trainingsetindex: int = 0, + comparison_bodyparts: str | list[str] = "all", + snapshotindex: str | int | None = None, + rescale: bool = False, + fulldata: bool = False, + show_errors: bool = True, + modelprefix: str = "", + returnjustfns: bool = True, + engine: Engine | None = None, +): + """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[snapshotindex],scale,net_type] + + This function is only implemented for tensorflow models/shuffles, and will throw + 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, + comparison_bodyparts, cfg, Snapshots[snapshotindex] + ) + Args: + config (str | Path): Full path of the config.yaml file. + shuffle (int): Shuffle index of the training dataset. Defaults to 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". + comparison_bodyparts (list of bodyparts): The average error will be computed for those body parts only (has to + be a subset of the body parts). Defaults to "all". + snapshotindex (str or int, optional): The index of the snapshot to return the evaluation data for. This can be + an integer (e.g. 5000) or a string (e.g. "snapshot-5000"). + If None, the snapshot index specified in the project configuration file will be used. Defaults to None. + rescale (bool): 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! Defaults to False. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Examples: + If you do not want to plot: + + deeplabcut.return_evaluate_network_data("/analysis/project/reaching-task/config.yaml", shuffle=[1]) + + If you want to plot: + + deeplabcut.return_evaluate_network_data( + "/analysis/project/reaching-task/config.yaml", + shuffle=[1], + plotting=True + ) + """ + config = Path(config) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import return_evaluate_network_data + + return return_evaluate_network_data( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparisonbodyparts=comparison_bodyparts, + Snapindex=snapshotindex, + rescale=rescale, + fulldata=fulldata, + show_errors=show_errors, + modelprefix=modelprefix, + returnjustfns=returnjustfns, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="batchsize", new="batch_size", since="3.0.0") +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def analyze_videos( + config: str | Path, + videos: list[str | Path], + video_extensions: str | Sequence[str] | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + gputouse: str | None = None, + save_as_csv: bool = False, + in_random_order: bool = True, + destfolder: str | Path | None = None, + batch_size: int | None = None, + cropping: list[int] | None = None, + TFGPUinference: bool = True, + dynamic: tuple[bool, float, int] = (False, 0.5, 10), + modelprefix: str = "", + robust_nframes: bool = False, + allow_growth: bool = False, + use_shelve: bool = False, + auto_track: bool = True, + n_tracks: int | None = None, + animal_names: list[str] | None = None, + calibrate: bool = False, + identity_only: bool = False, + use_openvino: str | None = None, + engine: Engine | None = None, + **torch_kwargs, +): + """Makes prediction based on a trained network. + + The index of the trained network is specified by parameters in the config file + (in particular the variable 'snapshotindex'). + + The labels are stored as MultiIndex Pandas Array, which contains the name of + the network, body part name, (x, y) label position in pixels, and the + likelihood for each frame per body part. These arrays are stored in an + efficient Hierarchical Data Format (HDF) 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 comma-separated values format (.csv), which in turn can be + imported in many programs, such as MATLAB, R, Prism, etc. + + Args: + config (str | Path): Full path of the config.yaml file. + videos (list[str | Path]): 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. + video_extensions (str | Sequence[str] | None, optional): Controls + how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). Defaults to None. + shuffle (int, optional): Shuffle index of the training dataset used for + training the network. Defaults to 1. + trainingsetindex (int, optional): Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). Defaults to 0. + gputouse (int or None, optional): Only for the TensorFlow engine (for the PyTorch engine see the + ``torch_kwargs``: you can use ``device``). Indicates the GPU to use (see number in ``nvidia-smi``). + If you do not have a GPU put ``None``. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + save_as_csv (bool, optional): Saves the predictions in a .csv file. Defaults to False. + in_random_order (bool, optional): Whether or not to analyze videos in a random order. + This is only relevant when specifying a video directory in `videos`. Defaults to True. + destfolder (str | Path | None, optional): Destination folder for analysis data. If ``None``, uses the + video path. Pass this folder for subsequent analysis too. Defaults to None. + batch_size (int or None, optional): Currently not supported by the PyTorch engine. Batch size for inference; + overwrites ``pose_cfg.yaml`` if set. Defaults to None. + cropping (list or None, optional): List of cropping coordinates as [x1, x2, y1, y2]. + Note that the same cropping parameters will then be used for all videos. + If different video crops are desired, run ``analyze_videos`` on individual + videos with the corresponding cropping coordinates. Defaults to None. + TFGPUinference (bool, optional): Only for the TensorFlow engine. Perform inference on GPU with TensorFlow code. + Introduced in "Pretraining boosts out-of-domain robustness for pose estimation" by Alexander Mathis, + Mert Yüksekgönül, Byron Rogers, Matthias Bethge, Mackenzie W. Mathis. + Source: https://arxiv.org/abs/1909.11229. Defaults to True. + 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. str: + """Creates a tracking dataset to train a ReID tracklet stitcher. + + Args: + config (str | Path): Full path of the config.yaml file. + videos (list[str | Path]): A list of strings containing the full paths to videos from which to create a tracking + dataset, or a path to the directory where all the videos with same extension are + stored. + track_method (str): Specifies the tracker used to generate the pose estimation data. Must be either 'box', + 'skeleton', or 'ellipse'. + video_extensions (str | Sequence[str] | None, optional): Controls how ``videos`` are filtered, + based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). Defaults to None. + shuffle (int, optional): Shuffle index of the training dataset used for training the network. Defaults to 1. + trainingsetindex (int, optional): Integer specifying which TrainingsetFraction to use. By default the first + (note that TrainingFraction is a list in config.yaml). Defaults to 0. + gputouse (int or None, optional): Only for the TensorFlow engine (for the PyTorch engine use ``device``). + Indicates the GPU to use (see number in ``nvidia-smi``). If you do not have a GPU put ``None``. + See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + TFGPUinference (bool, optional): Only for the TensorFlow engine. Perform inference on GPU with TensorFlow code. + Introduced in "Pretraining boosts out-of-domain robustness for pose estimation" by Alexander Mathis, + Mert Yüksekgönül, Byron Rogers, Matthias Bethge, Mackenzie W. Mathis. + Source: https://arxiv.org/abs/1909.11229. Defaults to True. + destfolder (str | Path | None, optional): Specifies the destination folder for analysis data. If ``None``, + the path of the video is used. Note that for subsequent analysis this folder also needs to be passed. + modelprefix (str, optional): Directory containing the deeplabcut models to use when evaluating the network. + By default, the models are assumed to exist in the project folder. Defaults to "". + robust_nframes (bool, optional): Evaluate a video's number of frames in a robust manner. This option is slower + (as the whole video is read frame-by-frame), but does not rely on metadata, hence its robustness against + file corruption. Defaults to False. + n_triplets (int): The number of triplets to extract for the dataset. Defaults to 1000. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Returns: + str: DLCScorer; the scorer used to analyze the videos. + """ + config = Path(config) + videos = _coerce_video_paths(videos) + destfolder = _coerce_optional_path(destfolder) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import create_tracking_dataset + + return create_tracking_dataset( + config, + videos, + track_method, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + destfolder=destfolder, + batchsize=batch_size, + cropping=cropping, + TFGPUinference=TFGPUinference, + modelprefix=modelprefix, + robust_nframes=robust_nframes, + n_triplets=n_triplets, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import create_tracking_dataset + + return create_tracking_dataset( + config, + videos, + track_method, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + destfolder=destfolder, + batch_size=batch_size, + cropping=cropping, + modelprefix=modelprefix, + robust_nframes=robust_nframes, + n_triplets=n_triplets, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def analyze_images( + config: str | Path, + images: str | Path | list[str] | list[Path], + frame_type: str | None = None, + destfolder: str | Path | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + max_individuals: int | None = None, + device: str | None = None, + snapshot_index: int | None = None, + detector_snapshot_index: int | None = None, + save_as_csv: bool = False, + modelprefix: str = "", + plotting: bool | str = False, + pcutoff: float | None = None, + bbox_pcutoff: float | None = None, + plot_skeleton: bool = False, + **torch_kwargs, +) -> dict[str, dict[str, np.ndarray | np.ndarray]]: + """Analyzes images with a DeepLabCut model and stores the output in an H5 file. + + This method is only implemented for PyTorch models. + + The labels are stored as Pandas DataFrame, which contains the name of the network, + body part name, (x, y) label position in pixels, and the likelihood for each frame + per body part. + + Args: + config (str, Path): Full path of the project's config.yaml file. + images (str, Path, list[str], list[Path]): The image(s) to run inference on. Can be the path to an image, the + path to a directory containing images, or a list of image paths or directories + containing images. + frame_type (string, optional): Filters the images to analyze to only the ones with the given suffix (e.g. + setting `frame_type`=".png" will only analyze ".png" images). + The default behavior analyzes all ".jpg", ".jpeg" and ".png" images. + destfolder (str, Path, optional): The directory where the predictions will be stored. If None, the predictions + will be + stored in the same directory as the first image given in the `images` argument (if it's a directory, that + directory will be used; if it's an image, the directory containing the image will be used). + shuffle (int, optional): An integer specifying the shuffle with which to run image analysis. + trainingsetindex (int, optional): Integer specifying which TrainingsetFraction to use. By default, the first one + is used (note that TrainingFraction is a list in config.yaml). + max_individuals (int, optional): The maximum number of individuals to detect in each image. Set to the number of + individuals in the project if None. + device (str, optional): The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. + For more information, see https://pytorch.org/docs/stable/notes/cuda.html + snapshot_index (int, optional): Index (starting at 0) of the snapshot to use for image analysis. To evaluate the + last one, use -1. Default uses the value set in the project config. + detector_snapshot_index (int, optional): Only for Top-Down PyTorch models. If defined, uses the detector with + the given index for pose estimation. To evaluate the last one, use -1. Default uses the value set in the + project config. + save_as_csv (bool, optional): Saves the predictions in a .csv file. If provided it must be either ``True`` or + ``False``. Defaults to False. + modelprefix (str, optional): Directory containing the deeplabcut models to use when running image analysis. By + default, the models are assumed to exist in the project folder. + plotting (bool, str): Plots the predictions made by the model on the analyzed images. Results will be stored in + a folder named `LabeledImages_{scorer}`, where scorer is the name of the model used to analyze the images. + This folder will be in the same directory as the file containing the predictions (either the given + `destfolder`, or the folder containing the first image to analyze). + If provided it must be either ``True``, ``False``, ``"bodypart"``, or ``"individual"``. Setting to ``True`` + defaults as ``"bodypart"`` for multi-animal projects. If a detector is used, the predicted bounding + boxes will also be plotted. Defaults to False. + pcutoff (float, optional): The cutoff score when plotting pose predictions. Must be None or in (0, 1). If None, + the pcutoff is read from the project configuration file. Defaults to None. + bbox_pcutoff (float, optional): The cutoff score when plotting bounding box predictions. Must be None or in (0, + 1). If None, it is read from the project configuration file. Defaults to None. + plot_skeleton (bool): If a skeleton is defined in the project's config.yaml, whether to plot the skeleton + connecting the predicted bodyparts on the images. Defaults to False. + torch_kwargs: Any extra parameters to pass to the PyTorch API, such as ``ctd_conditions``. + + Returns: + dict: A dictionary mapping image paths (as strings) to model predictions. + + Examples: + If you want to analyze all frames in /analysis/project/my_images: + + import deeplabcut + deeplabcut.analyze_images( + "/analysis/project/reaching-task/config.yaml", + "/analysis/project/my_images", + ) + + If you want to analyze two specific images with your shuffle 3 model: + + import deeplabcut + deeplabcut.analyze_images( + "/analysis/project/reaching-task/config.yaml", + images=["image_001.png", "img_002.jpg"], + shuffle=3, + ) + + If you want to analyze frames in a folder, save them and plot predictions: + + import deeplabcut + deeplabcut.analyze_images( + "/analysis/project/reaching-task/config.yaml", + "/analysis/project/my_images", + shuffle=3, + destfolder="/analysis/project/my_images_analyzed", + plotting=True, + ) + """ + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import analyze_images + + return analyze_images( + config=config, + images=images, + frame_type=frame_type, + output_dir=destfolder, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshot_index=snapshot_index, + detector_snapshot_index=detector_snapshot_index, + modelprefix=modelprefix, + device=device, + save_as_csv=save_as_csv, + max_individuals=max_individuals, + plotting=plotting, + pcutoff=pcutoff, + bbox_pcutoff=bbox_pcutoff, + plot_skeleton=plot_skeleton, + **torch_kwargs, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def analyze_time_lapse_frames( + config: str | Path, + directory: str | Path, + frametype: str = ".png", + shuffle: int = 1, + trainingsetindex: int = 0, + gputouse: int | None = None, + device: str | None = None, + save_as_csv: bool = False, + modelprefix: str = "", + engine: Engine | None = None, +): + """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. + + Output: The labels are stored as MultiIndex Pandas Array, which contains the name + of the network, body part name, (x, y) label position in pixels, and the likelihood + for each frame per body part. These arrays are stored in an efficient Hierarchical + Data Format (HDF) 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 comma-separated + values format (.csv), which in turn can be imported in many programs, such as + MATLAB, R, Prism, etc. + + Args: + config (str | Path): Full path of the config.yaml file. + directory (str | Path): 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. Defaults to ``.png``. + shuffle (int, optional): Shuffle index of the training dataset used for training the network. Defaults to 1. + trainingsetindex (int, optional): Integer specifying which TrainingsetFraction to use. By default the first + (note that TrainingFraction is a list in config.yaml). + gputouse (int | None, optional): Only for TensorFlow models. For PyTorch models, please use `device`. + Natural number indicating the number of your GPU (see number in nvidia-smi). If you do not have a GPU put + None. See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries + device (str, optional): The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. For more information, see + https://pytorch.org/docs/stable/notes/cuda.html + save_as_csv (bool, optional): Saves the predictions in a .csv file. If provided it must be either ``True`` or + ``False``. Defaults to False. + + Examples: + If you want to analyze all frames in /analysis/project/timelapseexperiment1: + + import deeplabcut + deeplabcut.analyze_time_lapse_frames( + '/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. + ```bash + ffmpeg -i testvideo.avi "thumb%04d.png" + ``` + """ + config = Path(config) + directory = Path(directory) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import analyze_time_lapse_frames + + return analyze_time_lapse_frames( + config, + directory, + frametype=frametype, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + save_as_csv=save_as_csv, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import analyze_images + + return analyze_images( + config=config, + images=directory, + output_dir=directory, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + device=_gpu_to_use_to_device(gputouse, device), + save_as_csv=save_as_csv, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def convert_detections2tracklets( + config: str | Path, + videos: list[str | Path], + video_extensions: str | Sequence[str] | None = None, + shuffle: int = 1, + trainingsetindex: int = 0, + overwrite: bool = False, + destfolder: str | Path | None = None, + ignore_bodyparts: list[str] | None = None, + inferencecfg: dict | None = None, + modelprefix: str = "", + greedy: bool = False, + calibrate: bool = False, + window_size: int = 0, + identity_only: int = False, + track_method: str = "", + engine: Engine | None = None, +): + """This should be called at the end of deeplabcut.analyze_videos for multianimal + projects! + + Args: + config (str | Path): Full path of the config.yaml file. + videos (list[str | Path]): 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. + video_extensions (str | Sequence[str] | None, optional): Controls how ``videos`` are filtered, + based on file extension. + File paths and directory contents are treated differently: - ``None`` (default): file paths are accepted + as-is; directories are scanned for files with a recognized video extension. - ``str`` or ``Sequence[str]`` + (e.g. ``"mp4"`` or ``["mp4", "avi"]``): both file paths and directory contents are filtered by the given + extension(s). Defaults to None. + shuffle (int, optional): Shuffle index of the training dataset used for training the network. Defaults to 1. + trainingsetindex (int, optional): 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 (str | Path | None, 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. + ignore_bodyparts (optional): List of body part names that should be ignored during tracking (advanced). By + default, all the body parts are used. + inferencecfg (optional): Configuration file for inference (assembly of individuals). Ideally should be obtained + from cross validation (during evaluation). By default the parameters are loaded from inference_cfg.yaml, but + these get_level_values can be overwritten. Defaults to None. + calibrate (bool, optional): If True, use training data to calibrate the animal assembly procedure. This improves + its robustness to wrong body part links, but requires very little missing data. Defaults to False. + window_size (int, optional): Recurrent connections in the past `window_size` frames are prioritized during + assembly. By default, no temporal coherence cost is added, and assembly is driven mainly by part affinity + costs. Defaults to 0. + identity_only (bool, optional): If True and animal identity was learned by the model, assembly and tracking rely + exclusively on identity prediction. Defaults to False. + track_method (string, optional): Specifies the tracker used to generate the pose estimation data. For multiple + animals, must be either 'box', 'skeleton', or 'ellipse' and will be taken from the config.yaml file if none + is given. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Examples: + If you want to convert detections to tracklets: + + import deeplabcut + deeplabcut.convert_detections2tracklets( + "/analysis/project/reaching-task/config.yaml", + ["/analysis/project/video1.mp4"], + video_extensions='.mp4', + ) + + If you want to convert detections to tracklets based on box_tracker: + + import deeplabcut + deeplabcut.convert_detections2tracklets( + "/analysis/project/reaching-task/config.yaml", + ["/analysis/project/video1.mp4"], + video_extensions=".mp4", + track_method="box", + ) + """ + config = Path(config) + videos = _coerce_video_paths(videos) + destfolder = _coerce_optional_path(destfolder) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import convert_detections2tracklets + + return convert_detections2tracklets( + config, + videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + overwrite=overwrite, + destfolder=destfolder, + ignore_bodyparts=ignore_bodyparts, + inferencecfg=inferencecfg, + modelprefix=modelprefix, + greedy=greedy, + calibrate=calibrate, + window_size=window_size, + identity_only=identity_only, + track_method=track_method, + ) + + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis import convert_detections2tracklets + + if greedy or calibrate or window_size: + raise NotImplementedError( + f"The 'greedy', 'calibrate' and 'window_size' option are not yet implemented with {engine}" + ) + + return convert_detections2tracklets( + config, + videos, + video_extensions=video_extensions, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + overwrite=overwrite, + destfolder=destfolder, + ignore_bodyparts=ignore_bodyparts, + inferencecfg=inferencecfg, + modelprefix=modelprefix, + identity_only=identity_only, + track_method=track_method, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def extract_maps( + config: str | Path, + shuffle: int = 0, + trainingsetindex: int = 0, + gputouse: int | None = None, + device: str | None = None, + rescale: bool = False, + Indices: list[int] | None = None, + modelprefix: str = "", + engine: Engine | None = None, +): + """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). + + Args: + config (str | Path): Full path of the config.yaml file. + shuffle (int): Shuffle index of the training dataset. Defaults to 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". + gputouse (int or None, optional): For the TensorFlow engine (for the PyTorch engine see ``device``). Specifies + the GPU to use (see number in ``nvidia-smi``). If you do not have a GPU put ``None``. See: + https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + device (str or None, optional): The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. See + https://pytorch.org/docs/stable/notes/cuda.html for more information. Defaults to None. + rescale (bool): 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! Defaults to False. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Examples: + 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]) + """ + config = Path(config) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import extract_maps + + return extract_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + gputouse=gputouse, + rescale=rescale, + Indices=Indices, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import extract_maps + + return extract_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + device=_gpu_to_use_to_device(gputouse, device), + rescale=rescale, + indices=Indices, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def visualize_scoremaps(image: np.ndarray, scmap: np.ndarray): + """Plots scoremaps as an image overlay. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + + Returns: + The figure and axis on which the image scoremap was plot. + """ + return visualization.visualize_scoremaps(image, scmap) + + +def visualize_locrefs( + image: np.ndarray, + scmap: np.ndarray, + locref_x: np.ndarray, + locref_y: np.ndarray, + step: int = 5, + zoom_width: int = 0, +): + """Plots a scoremap and the corresponding location refinement field on an image. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + locref_x: The x-coordinate of the location refinement field, of shape (h, w) + locref_y: The y-coordinate of the location refinement field, of shape (h, w) + step: The step with which to plot the location refinement field. + zoom_width: The zoom width with which to plot the scoremaps. + + 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) + + +def visualize_paf( + image: np.ndarray, + paf: np.ndarray, + step: int = 5, + colors: list | None = None, +): + """Plots the PAF on top of the image. + + Args: + image: Shape (height, width, channels). The image on which the model was run. + paf: Shape (height, width, 2 * len(paf_graph)). The PAF output by the model. + step: The step with which to plot the scoremaps. + colors: The colormap to use. + + Returns: + The figure and axis on which the image PAF was plot. + """ + return visualization.visualize_paf(image, paf, step=step, colors=colors) + + +@renamed_parameter(old="comparisonbodyparts", new="comparison_bodyparts", since="3.0.0") +def extract_save_all_maps( + config: str | Path, + shuffle: int = 1, + trainingsetindex: int = 0, + comparison_bodyparts: str | list[str] = "all", + extract_paf: bool = True, + all_paf_in_one: bool = True, + gputouse: int | None = None, + device: str | None = None, + rescale: bool = False, + Indices: list[int] | None = None, + modelprefix: str = "", + dest_folder: str | Path | None = None, + snapshot_index: int | str | None = None, + detector_snapshot_index: int | str | None = None, + engine: Engine | 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. + + Args: + config (str | Path): Full path of the config.yaml file. + shuffle (int): Shuffle index of the training dataset. Defaults to 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". + comparison_bodyparts (list of bodyparts): The average error will be computed for those body parts only + (has to be a subset of the body parts). Defaults to "all". + extract_paf (bool): Extract part affinity fields by default. Note that turning it off will make the function + much faster. + all_paf_in_one (bool): By default, all part affinity fields are displayed on a single frame. If false, + individual fields are shown on separate frames. + gputouse (int or None, optional): For the TensorFlow engine (for the PyTorch engine see ``device``). Specifies + the GPU to use (see number in ``nvidia-smi``). If you do not have a GPU put ``None``. See: + https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries. Defaults to None. + device (str or None, optional): The CUDA device to use for training. If None, the device will be taken from the + ``pytorch_config.yaml`` file. Examples: {"cpu", "cuda", "cuda:0", "cuda:1"}. See + https://pytorch.org/docs/stable/notes/cuda.html for more information. Defaults to None. + Indices: For which images shall the scmap/locref and paf be computed? Give a list of images. Defaults to None. + snapshot_index: Only for PyTorch models. Index (starting at 0) of the snapshot we want to extract maps with. To + evaluate the last one, use -1. To extract maps for all snapshots, use "all". Default uses the value set in + the project config. + detector_snapshot_index: Only for TD PyTorch models. If defined, uses the detector with the given index for pose + estimation. To extract maps for all detector snapshots, use "all". Default uses the value set in the project + config. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Examples: + Calculated maps for images 0, 1 and 33. + + deeplabcut.extract_save_all_maps( + "/analysis/project/reaching-task/config.yaml", shuffle=1, Indices=[0, 1, 33] + ) + """ + config = Path(config) + dest_folder = _coerce_optional_path(dest_folder) + if engine is None: + engine = get_shuffle_engine( + read_config(config), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import extract_save_all_maps + + return extract_save_all_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparisonbodyparts=comparison_bodyparts, + extract_paf=extract_paf, + all_paf_in_one=all_paf_in_one, + gputouse=gputouse, + rescale=rescale, + Indices=Indices, + modelprefix=modelprefix, + dest_folder=dest_folder, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch import extract_save_all_maps + + return extract_save_all_maps( + config, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + comparison_bodyparts=comparison_bodyparts, + extract_paf=extract_paf, + all_paf_in_one=all_paf_in_one, + device=_gpu_to_use_to_device(gputouse, device), + rescale=rescale, + indices=Indices, + modelprefix=modelprefix, + snapshot_index=snapshot_index, + detector_snapshot_index=detector_snapshot_index, + dest_folder=dest_folder, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def export_model( + cfg_path: str | Path, + shuffle: int = 1, + trainingsetindex: int = 0, + snapshotindex: int | None = None, + iteration: int = None, + TFGPUinference: bool = True, + overwrite: bool = False, + make_tar: bool = True, + wipepaths: bool = False, + without_detector: bool = False, + modelprefix: str = "", + engine: Engine | None = None, +) -> None: + """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 (and an + `exported-models-pytorch` directory for PyTorch models). + + Args: + cfg_path (str | Path): Path to the DLC Project config.yaml file. + shuffle (int, optional): The shuffle of the model to export. Defaults to 1. + trainingsetindex (int, optional): The index of the training fraction for the model you wish to export. Defaults + to 1. + snapshotindex (int, optional): The snapshot index for the weights you wish to export. If None, uses the + snapshotindex as defined in 'config.yaml'. Defaults to None. + iteration (int, optional): The model iteration (active learning loop) you wish to export. If None, the iteration + listed in the config file is used. + TFGPUinference (bool, optional): Use the tensorflow inference model? For inference using DeepLabCut-live, it is + recommended to set TFGPUinference=False. Defaults to True. + overwrite (bool, optional): If the model you wish to export has already been exported, whether to overwrite. + Defaults to False. + make_tar (bool, optional): Compress the exported directory to a tar file. This is necessary to export to the + model zoo, but not for live inference. Defaults to True. + wipepaths (bool, optional): Removes the actual path of your project and the init_weights from pose_cfg. + without_detector (bool, optional): PyTorch engine only. Exports top-down models without the detector. + engine (Engine, optional): The default behavior loads the engine for the shuffle from the metadata. You can + overwrite this by passing the engine as an argument, but this should generally not be done. Defaults to + None. + + Examples: + Export the first stored snapshot for model trained with shuffle 3: + + deeplabcut.export_model("/analysis/project/reaching-task/config.yaml", shuffle=3, snapshotindex=-1) + """ + cfg_path = Path(cfg_path) + if engine is None: + engine = get_shuffle_engine( + read_config(cfg_path), + trainingsetindex=trainingsetindex, + shuffle=shuffle, + modelprefix=modelprefix, + ) + + if engine == Engine.TF: + from deeplabcut.pose_estimation_tensorflow import export_model + + return export_model( + cfg_path=cfg_path, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshotindex=snapshotindex, + iteration=iteration, + TFGPUinference=TFGPUinference, + overwrite=overwrite, + make_tar=make_tar, + wipepaths=wipepaths, + modelprefix=modelprefix, + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.apis.export import export_model + + return export_model( + config=cfg_path, + shuffle=shuffle, + trainingsetindex=trainingsetindex, + snapshotindex=snapshotindex, + iteration=iteration, + overwrite=overwrite, + wipe_paths=wipepaths, + without_detector=without_detector, + modelprefix=modelprefix, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def _update_device(gpu_to_use: int | None, torch_kwargs: dict) -> None: + if "device" not in torch_kwargs and gpu_to_use is not None: + device = _gpu_to_use_to_device(gpu_to_use, device=None) + if device is not None: + torch_kwargs["device"] = device + + +def _gpu_to_use_to_device(gpu_to_use: int | None, device: str | None) -> str | None: + if device is None and gpu_to_use is not None: + if isinstance(gpu_to_use, int): + device = f"cuda:{gpu_to_use}" + else: + device = gpu_to_use + + return device diff --git a/deeplabcut/core/__init__.py b/deeplabcut/core/__init__.py new file mode 100644 index 0000000000..117d127147 --- /dev/null +++ b/deeplabcut/core/__init__.py @@ -0,0 +1,10 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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/core/config/__init__.py b/deeplabcut/core/config/__init__.py new file mode 100644 index 0000000000..76115065bc --- /dev/null +++ b/deeplabcut/core/config/__init__.py @@ -0,0 +1,44 @@ +from deeplabcut.core.config.base_config import DLCBaseConfig, DLCVersionedConfig +from deeplabcut.core.config.project_config import ProjectConfig +from deeplabcut.core.config.utils import ( + create_config_template, + create_config_template_3d, + edit_config, + ensure_plain_config, + get_yaml_dumper, + get_yaml_loader, + normalize_for_serialization, + pretty_print, + read_config, + read_config_as_dict, + resolve_alias, + resolve_aliases_in_dict, + write_config, + write_config_3d, + write_config_3d_template, + write_project_config, +) + +__all__ = [ + "DLCBaseConfig", + "DLCVersionedConfig", + "ProjectConfig", + "create_config_template", + "create_config_template_3d", + "edit_config", + "pretty_print", + "read_config", + "read_config_as_dict", + "resolve_alias", + "resolve_aliases_in_dict", + "write_config", + "write_config_3d", + "write_config_3d_template", + "write_project_config", + "get_yaml_loader", + "get_yaml_dumper", + "versioning", + "validation", + "normalize_for_serialization", + "ensure_plain_config", +] diff --git a/deeplabcut/core/config/base_config.py b/deeplabcut/core/config/base_config.py new file mode 100644 index 0000000000..13cbd62591 --- /dev/null +++ b/deeplabcut/core/config/base_config.py @@ -0,0 +1,433 @@ +from __future__ import annotations + +import functools +import logging +import sys +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator +from pydantic_core import ArgsKwargs +from ruamel.yaml.comments import CommentedMap +from typing_extensions import Self + +from deeplabcut.core.config import versioning as versioning +from deeplabcut.core.config.utils import ( + normalize_for_serialization, + pretty_print, + read_config_as_dict, + resolve_alias, + resolve_aliases_in_dict, + write_config, +) + +logger = logging.getLogger(__name__) + + +class DLCBaseConfig(BaseModel): + """Pydantic base for DeepLabCut configuration models. + + This class is used to create configuration models for DeepLabCut. + It provides a base class for all configuration models that need YAML/dict I/O + and optional deprecated field names via ``json_schema_extra["aliases"]``. + (Use for all nested configs, e.g. pytorch ``DataConfig``, ``InferenceConfig``, etc.) + + For project-level schema migration and dirty-field tracking, subclass + `DLCVersionedConfig` instead. + + Features: + + - Strict schema (`extra="forbid"`, `validate_assignment=True`). + - Load and save: `from_yaml`, `from_dict`, `from_any`, `to_yaml`, `to_dict`. + - Pretty-print via `print`. + - Hooks: `_post_yaml_load_updates`. + - Nested dot-notation via `select`. + - Dict-like access over declared fields (legacy compatibility). + - In-place bulk updates via `update`. + - Field aliases from `json_schema_extra`. + """ + + model_config = ConfigDict(extra="forbid", validate_assignment=True) + + # ------------------------------------------------------------------ + # Validation (before pydantic field validation) + # ------------------------------------------------------------------ + + @model_validator(mode="before") + @classmethod + def resolve_aliases_before_validate(cls, data: Any) -> Any: + """Resolves aliases to their canonical names. (Normalizes ArgsKwargs + input to a dict for downstream validation.) + + Args: + data: Raw validator input (`dict`, `ArgsKwargs`, or other). + + Returns: + A dict with canonical field names when input is ArgsKwargs or dict; + otherwise `data` unchanged. + """ + if isinstance(data, ArgsKwargs): + data: dict = cls._args_kwargs_to_dict(data) + if isinstance(data, dict): + return resolve_aliases_in_dict(data, cls._alias_map(), target=cls.__name__) + return data + + # ------------------------------------------------------------------ + # Construction + # ------------------------------------------------------------------ + + @classmethod + def from_dict(cls, cfg_dict: dict) -> Self: + return cls.model_validate(cfg_dict) + + @classmethod + def from_any( + cls, + config: Self | dict | str | Path, + ) -> Self: + if isinstance(config, cls): + return config + elif isinstance(config, str | Path): + return cls.from_yaml(config) + elif isinstance(config, dict): + return cls.from_dict(config) + else: + raise TypeError( + "Failure to load configuration: Expected a config instance, " + f"dictionary, string, or Path. Got {type(config)}" + ) + + # Note @deruyter92 2026-06-15: the ignore_empty option is currently just used to support + # some top-level fields in v0 legacy configs that are often empty. Should be removed in v1. + @classmethod + def from_yaml(cls, yaml_path: str | Path, ignore_empty: bool = True) -> Self: + yaml_dict = read_config_as_dict(yaml_path) + if ignore_empty: + yaml_dict = {k: v for k, v in yaml_dict.items() if v is not None} + cfg = cls.from_dict(yaml_dict) + cfg._post_yaml_load_updates(yaml_path=Path(yaml_path)) + return cfg + + # ------------------------------------------------------------------ + # Serialization + # ------------------------------------------------------------------ + + def to_commented_map(self) -> CommentedMap: + """Recursively convert the config to a CommentedMap with YAML comments.""" + dumped = self.to_dict(normalize=True) + data = CommentedMap() + for name, info in type(self).model_fields.items(): + extra = info.json_schema_extra + if isinstance(extra, dict) and (comment := extra.get("comment")): + data.yaml_set_comment_before_after_key(name, before=comment) + value = getattr(self, name) + if isinstance(value, DLCBaseConfig): + data[name] = value.to_commented_map() + else: + data[name] = dumped[name] + return data + + def to_yaml( + self, + yaml_path: str | Path, + *, + overwrite: bool = True, + ) -> None: + write_config(yaml_path, self.to_commented_map(), overwrite=overwrite) + + def to_dict(self, *, normalize: bool = False) -> dict: + if not normalize: + return self.model_dump() + return normalize_for_serialization(self.model_dump()) + + def print( + self, + indent: int = 0, + print_fn: Callable[[str], None] | None = None, + ) -> None: + pretty_print(config=self.to_dict(), indent=indent, print_fn=print_fn) + + # ------------------------------------------------------------------ + # Hooks (override in subclasses) + # ------------------------------------------------------------------ + + def _post_yaml_load_updates(self, *, yaml_path: Path) -> None: + pass + + # ------------------------------------------------------------------ + # Field aliases (deprecated names in json_schema_extra) + # ------------------------------------------------------------------ + + @classmethod + @functools.cache + def _alias_map(cls) -> dict[str, str]: + """Build a map of deprecated aliases to canonical field names. + + Returns: + Dict mapping each alias in `json_schema_extra["aliases"]` to its + canonical field name. + + Raises: + ValueError: If the same alias is declared on more than one field. + """ + mapping: dict[str, str] = {} + for name, info in cls.model_fields.items(): + extra = info.json_schema_extra + if not isinstance(extra, dict): + continue + for alias in extra.get("aliases", []): + if alias in mapping: + raise ValueError(f"Duplicate alias '{alias}' for fields '{mapping[alias]}' and '{name}'") + mapping[alias] = name + return mapping + + def _resolve_alias( + self, + name: str, + *, + warn: bool = True, + stacklevel: int = 4, + ) -> str: + return resolve_alias(name, type(self)._alias_map(), warn=warn, stacklevel=stacklevel) + + # ------------------------------------------------------------------ + # Dict-like access (canonical field names only in keys()/iter) + # ------------------------------------------------------------------ + + def __setattr__(self, name: str, value: Any) -> None: + name = self._resolve_alias(name) + super().__setattr__(name, value) + + def __getattr__(self, name: str) -> Any: + # Only runs after normal lookup fails; try resolved alias or raise AttributeError via BaseModel.__getattr__. + if name in type(self)._alias_map(): + return getattr(self, self._resolve_alias(name)) + return super().__getattr__(name) + + def __getitem__(self, key: str) -> Any: + key = self._resolve_alias(key) + try: + return getattr(self, key) + except AttributeError: + raise KeyError(key) from None + + def __setitem__(self, key: str, value: Any) -> None: + canonical = self._resolve_alias(key, warn=True) + if canonical not in self._field_names(): + raise KeyError(f"'{type(self).__name__}' has no field '{key}'") + setattr(self, canonical, value) + + def __contains__(self, key: object) -> bool: + if not isinstance(key, str): + return False + if key in self._field_names(): + return True + return key in type(self)._alias_map() + + def __iter__(self) -> Iterator[str]: + return iter(self._field_names()) + + def __len__(self) -> int: + return len(self._field_names()) + + def get(self, key: str, default: Any = None) -> Any: + try: + return self[key] + except KeyError: + return default + + def update( + self, + updates: dict[str, Any] | None = None, + /, + **kwargs: Any, + ) -> Self: + if updates is not None and kwargs: + raise TypeError(f"{type(self).__name__}.update() accepts either a dict or keyword args, not both.") + overrides = updates if updates is not None else kwargs + # Resolve aliases together (checks for duplicate canonical field names) + resolved = resolve_aliases_in_dict(overrides, type(self)._alias_map(), target=type(self).__name__) + for name, value in resolved.items(): + setattr(self, name, value) + return self + + def keys(self) -> list[str]: + return self._field_names() + + def values(self) -> list[Any]: + return [getattr(self, name) for name in self._field_names()] + + def items(self) -> list[tuple[str, Any]]: + return [(name, getattr(self, name)) for name in self._field_names()] + + def select(self, path: str, default: Any = None) -> Any: + """Select a value from the config using dot notation for nested keys.""" + try: + return self._navigate_nested(fields=path.split(".")) + except (AttributeError, KeyError, TypeError): + return default + + def set_nested(self, path: str, value: Any) -> Self: + if "." not in path: + setattr(self, path, value) + return self + parts = path.split(".") + parent_fields, final_field = parts[:-1], parts[-1] + try: + parent = self._navigate_nested(parent_fields) + parent[final_field] = value + except (AttributeError, KeyError, TypeError) as e: + raise AttributeError(f"{type(self).__name__} has no '{path}'") from e + return self + + def _navigate_nested(self, fields: list[str]) -> Any: + """Navigate a nested structure of fields. Raises AttributeError / KeyError if any field is not found.""" + obj: Any = self + for field in fields: + obj = obj[field] if isinstance(obj, dict) else getattr(obj, field) + return obj + + def _field_names(self) -> list[str]: + cls = type(self) + if not isinstance(self, BaseModel): + raise TypeError(f"{cls.__name__} must inherit from pydantic.BaseModel") + return list(cls.model_fields.keys()) + + @classmethod + def _args_kwargs_to_dict(cls, data: ArgsKwargs) -> dict: + """Map positional and keyword constructor args to a field-name dict.""" + names = list(cls.model_fields.keys()) + return dict( + zip(names, data.args or [], strict=False), + **(data.kwargs or {}), + ) + + +class DLCVersionedConfig(DLCBaseConfig): + """Top-level configs with schema migration and change tracking. + + Subclass of `DLCBaseConfig` for project and pose YAML configs such as + `ProjectConfig` and `PoseConfig`. + + Note: + Pydantic runs `migrate_before_validate` before the base + `resolve_aliases_before_validate` (child-first order): schema migration + on legacy keys, then alias resolution for the current model. + + Additional behavior: + + - `migrate_before_validate` upgrades raw dicts to `CURRENT_CONFIG_VERSION`. + - Tracks fields modified after load; `to_yaml` can log changes and mark clean. + - Patches `__setattr__` once per class to record dirty fields while delegating + alias warnings to the base `__setattr__`. + """ + + config_version: int = Field( + default=versioning.CURRENT_CONFIG_VERSION, + json_schema_extra={"comment": "Config schema version. Do not edit manually."}, + ) + + _initialized: bool = PrivateAttr(default=False) + _dirty_fields: set[str] = PrivateAttr(default_factory=set) + _change_notes: dict[str, Any] = PrivateAttr(default_factory=dict) + + # ------------------------------------------------------------------ + # Version migration (before pydantic field validation) + # ------------------------------------------------------------------ + + @classmethod + def from_dict(cls, cfg_dict: dict) -> Self: + cfg_dict = versioning.migrate_config( + cfg_dict, + config_type=cls.__name__, + target_version=versioning.CURRENT_CONFIG_VERSION, + ) + return super().from_dict(cfg_dict) + + # ------------------------------------------------------------------ + # Serialization + # ------------------------------------------------------------------ + + def to_yaml( + self, + yaml_path: str | Path, + *, + overwrite: bool = True, + log_changes: bool = True, + mark_clean: bool = True, + ) -> None: + super().to_yaml(yaml_path, overwrite=overwrite) + if log_changes: + self.log_changes() + if mark_clean: + self.mark_clean() + + # ------------------------------------------------------------------ + # Change tracking + # ------------------------------------------------------------------ + + def model_post_init(self, __context: Any) -> None: + super().model_post_init(__context) + self._initialized = True + + def __setattr__(self, name: str, value: Any) -> None: + name = self._resolve_alias(name) + + # Private attributes (not a model field) skip tracking logic + if name not in type(self).model_fields or not self._initialized: + super().__setattr__(name, value) + return + + # Get the coerced values before and after setting; log changes + old_value = getattr(self, name, None) + super().__setattr__(name, value) + new_value = getattr(self, name) + if old_value != new_value: + self._dirty_fields.add(name) + + @property + def is_dirty(self) -> bool: + return bool(self._dirty_fields) + + @property + def dirty_fields(self) -> frozenset[str]: + return frozenset(self._dirty_fields) + + @property + def change_notes(self) -> list[str]: + return list(self._change_notes.values()) + + def record_change_note( + self, + field_name: str, + message: str, + *, + include_caller: bool = False, + _stack_depth: int = 1, + ) -> None: + field_name = self._resolve_alias(field_name) + + if field_name not in type(self).model_fields: + raise KeyError(f"'{type(self).__name__}' has no field '{field_name}'") + + if include_caller: + frame = sys._getframe(_stack_depth) + filename = frame.f_code.co_filename.rsplit("/", 1)[-1] + message = f"{message} [{filename}:{frame.f_lineno}]" + + self._change_notes[field_name] = message + + def log_changes(self) -> None: + if not self.is_dirty: + return + logger.info(f"Updates to {type(self).__name__}:") + for field_name in sorted(self._dirty_fields): + if field_name in self._change_notes: + logger.info(f" {self._change_notes[field_name]}") + else: + logger.info(f" {field_name} was modified") + + def mark_clean(self) -> None: + self._dirty_fields.clear() + self._change_notes.clear() diff --git a/deeplabcut/core/config/project_config.py b/deeplabcut/core/config/project_config.py new file mode 100644 index 0000000000..40481e0af9 --- /dev/null +++ b/deeplabcut/core/config/project_config.py @@ -0,0 +1,275 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Project configuration classes for DeepLabCut pose estimation models.""" + +from pathlib import Path +from typing import Any, Literal + +from pydantic import Field, model_validator +from typing_extensions import Self + +from deeplabcut.core.config.base_config import DLCVersionedConfig +from deeplabcut.core.config.validation import ( + BodypartPair, + Fraction, + NonNegativeInt, + StrictPositiveInt, + UniqueStrList, + less_than, + validate_crop_bounds, +) + + +class ProjectConfig(DLCVersionedConfig): + """Complete project configuration. + + Mirrors the structure of the project config.yaml (and metadata in pose config). + Field names match the old dictionary keys for round-trip compatibility. + + Attributes: + Task: Project task identifier (do not edit). + scorer: Scorer name (do not edit). + date: Project date (do not edit). + multianimalproject: Whether the project is multi-animal. + identity: Whether identity tracking is enabled (project config.yaml key). + project_path: Path to the DeepLabCut project. + pose_config_path: Path to the pose configuration file (metadata only). + engine: Default DeepLabCut engine (e.g. pytorch). + video_sets: Video set configuration. + bodyparts: List of body parts. + individuals: List of individual animal identities (multi-animal). + uniquebodyparts: List of unique body parts (multi-animal project key). + multianimalbodyparts: List of multi-animal body parts (multi-animal key). + start: Fraction of video to start extracting frames. + stop: Fraction of video to stop extracting frames. + numframes2pick: Number of frames to pick for labeling. + skeleton: Skeleton connectivity for plotting. + skeleton_color: Skeleton color for plotting. + pcutoff: Confidence cutoff for plotting. + dotsize: Dot size for visualization. + alphavalue: Alpha value for visualization. + colormap: Colormap for visualization. + TrainingFraction: Training fractions for dataset splits. + iteration: Training iteration. + default_net_type: Default network architecture. + default_augmenter: Default data augmenter. + default_track_method: Default tracking method. + snapshotindex: Snapshot index for evaluation. + detector_snapshotindex: Detector snapshot index. + batch_size: Training batch size. + detector_batch_size: Detector batch size. + cropping: Whether cropping is enabled for analysis. + x1: Cropping x1 coordinate. + x2: Cropping x2 coordinate. + y1: Cropping y1 coordinate. + y2: Cropping y2 coordinate. + corner2move2: Refinement corner configuration. + move2corner: Refinement move-to-corner flag. + SuperAnimalConversionTables: Conversion tables for SuperAnimal weights. + """ + + # Project definitions (do not edit) + Task: str = Field(default="", json_schema_extra={"comment": "Project definitions (do not edit)"}) + scorer: str = "" + date: str = "" + multianimalproject: bool = False + identity: bool | None = Field(default=None, json_schema_extra={"aliases": ["with_identity"]}) + + # Project path + project_path: Path = Field( + default_factory=Path, + json_schema_extra={"comment": "\nProject path (change when moving around)"}, + ) + pose_config_path: Path | None = None + + # Engine + engine: Literal["pytorch", "tensorflow"] = Field( + default="pytorch", + json_schema_extra={ + "comment": "\nDefault DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow)" + }, + ) + + # Annotation dataset configuration (and individual video cropping parameters) + video_sets: dict[str, Any] = Field( + default_factory=dict, + json_schema_extra={"comment": "\nAnnotation data set configuration (and individual video cropping parameters)"}, + ) + # VV TODO @deruyter92 2026-01-30: following the old original config.yaml template for now. VV + # VV We should change this to a list[str] in the future. VV + bodyparts: UniqueStrList | Literal["MULTI!"] = Field(default_factory=list) + + # TODO @deruyter92 2026-02-06: The current pipeline requires at least one individual defined in the + # default configuration. This will be removed in the future. + individuals: UniqueStrList = Field(default_factory=lambda: ["individual_1"]) + uniquebodyparts: UniqueStrList = Field(default_factory=list, json_schema_extra={"aliases": ["unique_bodyparts"]}) + multianimalbodyparts: UniqueStrList = Field(default_factory=list) # multi-animal project key + + # Fraction of video to start/stop when extracting frames for labeling/refinement + start: Fraction = Field( + default=0.0, + json_schema_extra={ + "comment": "\nFraction of video to start/stop when extracting frames for labeling/refinement" + }, + ) + stop: Fraction = 1.0 + numframes2pick: NonNegativeInt = 20 + + # Plotting configuration + skeleton: list[BodypartPair] = Field( + default_factory=list, + json_schema_extra={"comment": "\nPlotting configuration"}, + ) + skeleton_color: str = "black" + pcutoff: Fraction = 0.6 + dotsize: NonNegativeInt = 12 + alphavalue: Fraction = 0.7 + colormap: str = "rainbow" + + # Training, evaluation and analysis configuration + TrainingFraction: list[Fraction] = Field( + default_factory=lambda: [0.95], + json_schema_extra={"comment": "\nTraining,Evaluation and Analysis configuration"}, + ) + iteration: NonNegativeInt | None = None + default_net_type: str = "resnet_50" + default_augmenter: str | None = None + default_track_method: str | None = None + snapshotindex: Literal["all"] | int = "all" + detector_snapshotindex: int = -1 + batch_size: StrictPositiveInt = 8 + detector_batch_size: StrictPositiveInt = 1 + + # Cropping parameters (for analysis and outlier frame detection) + cropping: bool = Field( + default=False, + json_schema_extra={"comment": "\nCropping Parameters (for analysis and outlier frame detection)"}, + ) + x1: NonNegativeInt | None = Field( + default=None, + json_schema_extra={"comment": "if cropping is true for analysis, then set the values here:"}, + ) + x2: NonNegativeInt | None = None + y1: NonNegativeInt | None = None + y2: NonNegativeInt | None = None + + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) + corner2move2: list[NonNegativeInt] | None = Field( + default=None, + json_schema_extra={ + "comment": ( + "\nRefinement configuration (parameters from annotation dataset " + "configuration also relevant in this stage)" + ) + }, + ) + move2corner: bool | None = None + + # Conversion tables to fine-tune SuperAnimal weights + SuperAnimalConversionTables: dict[str, Any] | None = Field( + default=None, + json_schema_extra={"comment": "\nConversion tables to fine-tune SuperAnimal weights"}, + ) + + # TODO @deruyter92 2026-02-06: These parameters are no longer used in the new pipeline. + # We should remove them in config schema v1. They are needed now to support reading old configs. + resnet: int | None = Field( + default=None, + json_schema_extra={ + "comment": "\nThese are very old parameters that are no longer used They are simply ignored." + }, + ) + croppedtraining: bool | None = None + + @property + def bodyparts_list(self) -> list[str]: + # Animal-count agnostic; Always return a list (never "MULTI!", None, etc.) + if self.multianimalproject: + return list(self.multianimalbodyparts) + if self.bodyparts == "MULTI!": + raise ValueError("bodyparts must be a list of strings when multianimalproject is False, got 'MULTI!'") + return list(self.bodyparts) + + @property + def config_yaml_path(self) -> Path: + return self.project_path / "config.yaml" + + def validate_project_path(self, *, yaml_path: str | Path | None = None, write: bool = False) -> Self: + """Repair project_path from yaml location; optionally persist.""" + path = Path(yaml_path) if yaml_path is not None else self.config_yaml_path + if not path.is_file(): + raise FileNotFoundError(f"config.yaml not found: {path}") + self._post_yaml_load_updates(yaml_path=path) + if write and "project_path" in self.dirty_fields: + self.to_yaml(path, log_changes=True, mark_clean=True) + return self + + @classmethod + def from_any(cls, config: Self | dict | str | Path, *, repair_path: bool = False) -> Self: + cfg = super().from_any(config) + if repair_path: + cfg.validate_project_path(yaml_path=config if isinstance(config, (str, Path)) else None, write=True) + return cfg + + def _post_yaml_load_updates(self, *, yaml_path: Path) -> None: + """ + Override method for post-yaml load updates. Called automatically by from_yaml(). + These are logged but not written to disk -- call to_yaml() explicitly if needed. + """ + project_path = yaml_path.parent + if project_path.absolute() != self.project_path.absolute(): + old = self.project_path + self.project_path = project_path + self.record_change_note( + "project_path", + f"project_path updated: {old} -> {project_path} (resolved from YAML location when reading config.yaml)", + ) + + @model_validator(mode="before") + @classmethod + def normalize_legacy_empty_values(cls, data: Any) -> Any: + if not isinstance(data, dict): + return data + data = dict(data) + + # Some old configs used empty strings for unset fields + for fieldname in ("skeleton", "TrainingFraction", "video_sets", "bodyparts"): + if data.get(fieldname) == "": + data.pop(fieldname) + + # NOTE @deruyter92 2026-06-15: This should be removed in v1. + if data.get("multianimalproject") and not data.get("bodyparts"): + data["bodyparts"] = "MULTI!" + + return data + + @model_validator(mode="after") + def validate_start_before_stop(self) -> Self: + less_than(self.start, self.stop, name="start", threshold_name="stop") + return self + + @model_validator(mode="after") + def validate_bodyparts_single_animal(self) -> Self: + if not self.multianimalproject and self.bodyparts == "MULTI!": + raise ValueError("bodyparts must be a list of strings when multianimalproject is False, got 'MULTI!'") + + # TODO @deruyter92 2026-06-15: This sentinel should be removed in v1. + elif self.multianimalproject and self.bodyparts != "MULTI!": + raise ValueError(f"bodyparts must be 'MULTI!' when multianimalproject is True, got {self.bodyparts}") + + return self + + @model_validator(mode="after") + def validate_cropping_bounds(self) -> Self: + if self.cropping and any(value is None for value in (self.x1, self.x2, self.y1, self.y2)): + raise ValueError("When cropping is enabled, x1, x2, y1, and y2 must all be set") + validate_crop_bounds(x1=self.x1, x2=self.x2, y1=self.y1, y2=self.y2) + return self diff --git a/deeplabcut/core/config/utils.py b/deeplabcut/core/config/utils.py new file mode 100644 index 0000000000..cc0005bb76 --- /dev/null +++ b/deeplabcut/core/config/utils.py @@ -0,0 +1,428 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Centralized helpers for reading, writing, and creating configuration files (YAML).""" + +from __future__ import annotations + +import logging +import warnings +from collections import Counter +from collections.abc import Callable, Mapping +from enum import Enum +from functools import wraps +from pathlib import Path, PurePath +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from deeplabcut.core.config.project_config import ProjectConfig +import numpy as np +import ruamel.yaml.representer +from pydantic import ValidationError +from ruamel.yaml import YAML + +from deeplabcut.core.deprecation import deprecated + +logger = logging.getLogger(__name__) + + +def get_yaml_loader() -> YAML: + """Get a ruamel.yaml YAML handler with safe mode.""" + yaml = YAML(typ="safe", pure=True) + return yaml + + +def get_yaml_dumper() -> YAML: + """Get a ruamel.yaml YAML handler with representers for Enum and Path objects.""" + yaml = YAML(typ="rt", pure=True) + + # Use a very large width so long strings (e.g., file paths or keys with spaces) + # are kept on a single line instead of being wrapped, which can otherwise cause + # them to be emitted as complex keys. See also: + # https://stackoverflow.com/questions/31197268/pyyaml-yaml-dump-produces-complex-key-for-string-key-122-chars/31199123#31199123 + # See PR https://github.com/DeepLabCut/DeepLabCut/pull/3140 for more details. + yaml.width = 1_000_000 + + # Auto-serialize Path objects as strings + yaml.representer.add_multi_representer(PurePath, lambda r, p: r.represent_str(str(p))) + yaml.representer.add_multi_representer(Enum, lambda r, e: r.represent_str(e.value)) + return yaml + + +def read_config_as_dict(config_path: str | Path) -> dict: + """ + Args: + config_path: the path to the configuration file to load + + Returns: + The configuration file with pure Python classes + + Raises: + FileNotFoundError: if the config file does not exist + """ + if not Path(config_path).exists(): + raise FileNotFoundError(f"Config {config_path} is not found. Please make sure that the file exists.") + with open(config_path) as f: + cfg = get_yaml_loader().load(f) + if cfg is None: + raise ValueError(f"Config {config_path} is empty or null.") + if not isinstance(cfg, dict): + raise ValueError(f"Config {config_path} must be a YAML mapping at the top level, got {type(cfg).__name__}.") + return cfg + + +def write_config(config_path: str | Path, config: dict, overwrite: bool = True) -> None: + """Writes a pose configuration file to disk. + + Args: + config_path: the path where the config should be saved + config: the config to save + overwrite: whether to overwrite the file if it already exists + + Raises: + 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") + + with open(config_path, "w") as file: + get_yaml_dumper().dump(config, file) + + +def resolve_alias( + name: str, + alias_map: dict[str, str], + *, + warn: bool = True, + stacklevel: int = 3, +) -> str: + """Resolve a config key to its canonical field name. + Args: + name: Raw key name (alias or canonical). + alias_map: ``{alias: canonical_name}`` for deprecated keys. + warn: If True, emit :class:`DLCDeprecationWarning` when ``name`` is an alias. + stacklevel: Passed to :func:`warnings.warn` for deprecation messages. + Returns: + Canonical field name, or ``name`` unchanged if it is not an alias. + """ + canonical = alias_map.get(name, name) + if warn and name in alias_map: + from deeplabcut.core.deprecation import DLCDeprecationWarning + + warnings.warn( + f"'{name}' is deprecated, use '{canonical}' instead.", + DLCDeprecationWarning, + stacklevel=stacklevel, + ) + return canonical + + +def resolve_aliases_in_dict( + cfg_dict: dict, + alias_map: dict[str, str], + *, + target: str = "config", + warn: bool = True, + stacklevel: int = 3, +) -> dict: + """Rename deprecated config keys to their canonical names. + + Args: + cfg_dict: Raw configuration mapping (e.g. from YAML). + alias_map: ``{alias: canonical_name}`` for deprecated keys. + target: Config class name shown in errors. + stacklevel: Passed to :func:`warnings.warn` for deprecation messages. + + Returns: + A new dict with alias keys replaced by canonical names. Unchanged if + ``alias_map`` is empty. + + Raises: + TypeError: If multiple keys resolve to the same canonical field name + (e.g. an alias and its canonical name, or two aliases for one field). + """ + if not alias_map: + return cfg_dict + + def _raise_for_duplicates(raw_to_canonical: dict[str, str]): + counts = Counter(raw_to_canonical.values()) + conflicts = [f"{raw} -> {canonical}" for raw, canonical in raw_to_canonical.items() if counts[canonical] > 1] + if conflicts: + raise TypeError(f"{target} received duplicate canonical field names: {', '.join(conflicts)}.") + + raw_to_canonical = {raw: resolve_alias(raw, alias_map, warn=warn, stacklevel=stacklevel + 1) for raw in cfg_dict} + _raise_for_duplicates(raw_to_canonical) + return {raw_to_canonical[raw]: v for raw, v in cfg_dict.items()} + + +def normalize_for_serialization(obj: Any) -> Any: + """Recursively normalize Paths to strings and Enums to values.""" + if isinstance(obj, Path): + return str(obj) + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, Mapping): + return type(obj)({k: normalize_for_serialization(v) for k, v in obj.items()}) + if isinstance(obj, tuple): + return tuple(normalize_for_serialization(v) for v in obj) + if isinstance(obj, (list, set)): + return [normalize_for_serialization(v) for v in obj] + if isinstance(obj, np.ndarray): + return obj.tolist() + return obj + + +def pretty_print( + config: dict, + indent: int = 0, + print_fn: Callable[[str], None] | None = None, +) -> None: + """Prints a model configuration in a pretty and readable way. + + Args: + config: the config to print + indent: the base indent on all keys + print_fn: custom function to call (simply calls ``print`` if None) + """ + if print_fn is None: + print_fn = print + + for k, v in config.items(): + if isinstance(v, dict): + print_fn(f"{indent * ' '}{k}:") + pretty_print(v, indent + 2, print_fn=print_fn) + else: + print_fn(f"{indent * ' '}{k}: {v}") + + +def ensure_plain_config(fn: Callable) -> Callable: + """Convert typed config arguments into plain Python objects. + + Any positional or keyword argument that is a DLCBaseConfig is converted to + a plain ``dict`` before the decorated function is called. + """ + + def _to_plain(value, fn_name: str = "", var_name: str = ""): + # Lazy import to avoid circular imports during module initialization. + from deeplabcut.core.config.base_config import DLCBaseConfig + + if isinstance(value, DLCBaseConfig): + logger.debug( + "converting %s (%s) to native dict in %s.", + var_name, + type(value).__name__, + fn_name, + ) + return value.to_dict() + return value + + @wraps(fn) + def wrapper(*args, **kwargs): + fn_name = fn.__qualname__ + args = tuple(_to_plain(a, fn_name) for a in args) + kwargs = {k: _to_plain(v, fn_name=fn_name, var_name=k) for k, v in kwargs.items()} + return fn(*args, **kwargs) + + return wrapper + + +# ----------------------------------------------------------------------------- +# Project config (config.yaml with template and defaults) +# ----------------------------------------------------------------------------- + + +@deprecated(replacement="deeplabcut.core.config.ProjectConfig", since="3.0.1") +def create_config_template(multianimal: bool = False) -> tuple: + """ + Creates a template for config.yaml file. This specific order is preserved while saving as yaml file. + + Returns: + (cfg_file, ruamelFile) for further editing and dumping. + """ + from deeplabcut.core.config.project_config import ProjectConfig + + ruamelFile = get_yaml_dumper() + + # TODO @deruyter92 2026-06-15: This sentinel should be removed in v1. + bodyparts = "MULTI!" if multianimal else [] + cfg_file = ProjectConfig(multianimalproject=multianimal, bodyparts=bodyparts).to_dict() + return cfg_file, ruamelFile + + +def create_config_template_3d() -> tuple: + """ + Creates a template for config.yaml file for 3d project. This specific order is preserved while saving as yaml file. + + Returns: + (cfg_file_3d, ruamelFile_3d) for further editing and dumping. + """ + yaml_str = """\ +# Project definitions (do not edit) +Task: +scorer: +date: +\n +# Project path (change when moving around) +project_path: +\n +# Plotting configuration +skeleton: # Note that the pairs must be defined, as you want them linked! +skeleton_color: +pcutoff: +colormap: +dotsize: +alphaValue: +markerType: +markerColor: +\n +# Number of cameras, camera names, path of the config files, shuffle index and trainingsetindex used to analyze videos: +num_cameras: +camera_names: +scorername_3d: # Enter the scorer name for the 3D output + """ + ruamelFile_3d = get_yaml_dumper() + cfg_file_3d = ruamelFile_3d.load(yaml_str) + return cfg_file_3d, ruamelFile_3d + + +def read_config(configname: str | Path, ignore_empty: bool = True) -> ProjectConfig: + """ + Reads structured config file defining a project. + + Applies default values and repairs (engine, detector_snapshotindex, project_path) + and writes back if needed. + + Args: + configname: Path to the project configuration file (config.yaml). + ignore_empty: If True, empty/None values in the YAML are ignored and + dataclass defaults are used instead. If False, empty values represent None. + Defaults to True. + + Returns: + The project configuration as a ProjectConfig instance (supports dict-like access). + """ + from deeplabcut.core.config.project_config import ProjectConfig + + path = Path(configname) + project_config = ProjectConfig.from_yaml(path, ignore_empty=ignore_empty) + + # If necessary, ProjectConfig automatically updates its project path via _post_yaml_load_updates. + # if that is the case (marked as dirty), we write the config back to the file. + if "project_path" in project_config.dirty_fields: + # NOTE @deruyter92 2026-02-02: copied old behaviour of writing the config + # immediately back to the file after reading it. We should consider separating + # the writing and reading instead of having inplace edits during reading. + project_config.to_yaml(configname, log_changes=True, mark_clean=True) + return project_config + + +def write_project_config( + configname: str | Path, + cfg: dict | ProjectConfig, +) -> None: + """ + Write structured project config file (config.yaml) preserving template order. + + Args: + configname (str | Path): Path to the project configuration file (config.yaml). + cfg (dict | ProjectConfig): The project configuration to write (requires ProjectConfig schema). + + Note: + Validates before writing when possible, unvalidated legacy dump on failure; This may not round-trip via + read_config for non-conformant legacy configurations. + """ + from deeplabcut.core.config.base_config import DLCBaseConfig + from deeplabcut.core.config.project_config import ProjectConfig + + try: + project_config: ProjectConfig = ProjectConfig.from_any(cfg) + project_config.to_yaml(configname) + return + except ValidationError as e: + logger.error( + "Invalid configuration! Validation error in project config file %s. Error: %s " + "Trying to write the file to disk anyway. Please verify the config file.", + cfg, + e, + ) + if isinstance(cfg, DLCBaseConfig): + logger.error("Expected a ProjectConfig, got %s.", type(cfg).__name__) + cfg.to_yaml(configname) + return + + warnings.warn("Reverting to legacy config file writing..", stacklevel=2) + with open(configname, "w") as cf: + cfg_file, ruamelFile = create_config_template(cfg.get("multianimalproject", False)) + for key in cfg.keys(): + cfg_file[key] = cfg[key] + + # Adding default value for variable skeleton and skeleton_color for backward compatibility. + if "skeleton" not in cfg.keys(): + cfg_file["skeleton"] = [] + cfg_file["skeleton_color"] = "black" + ruamelFile.dump(cfg_file, cf) + + +def edit_config(configname: str | Path, edits: dict, output_name: str | Path = "") -> dict: + """ + Convenience function to edit and save a config file from a dictionary. + + Note: + Legacy helper without schema validation. Prefer manipulating and saving + the typed config instead (e.g. cfg.update(edits).to_yaml(cfg_path)) + + Parameters + ---------- + configname : string + String containing the full path of the config file in the project. + edits : dict + Key–value pairs to edit in config + output_name : string, optional (default='') + Overwrite the original config.yaml by default. + If passed in though, new filename of the edited config. + + Examples + -------- + config_path = 'my_stellar_lab/dlc/config.yaml' + + edits = {'numframes2pick': 5, + 'trainingFraction': [0.5, 0.8], + 'skeleton': [['a', 'b'], ['b', 'c']]} + + deeplabcut.core.config.edit_config(config_path, edits) + """ + cfg = read_config_as_dict(configname) + for key, value in edits.items(): + cfg[key] = value + if not output_name: + output_name = configname + try: + write_config(output_name, cfg) + except ruamel.yaml.representer.RepresenterError: + 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_config(output_name, cfg) + return cfg + + +def write_config_3d(configname: str | Path, cfg: dict) -> None: + """Write structured 3D project config file.""" + with open(configname, "w") as cf: + cfg_file, ruamelFile = create_config_template_3d() + for key in cfg.keys(): + cfg_file[key] = cfg[key] + ruamelFile.dump(cfg_file, cf) + + +def write_config_3d_template(projconfigfile: str | Path, cfg_file_3d: dict, ruamelFile_3d: YAML) -> None: + """Write 3D config from pre-built template and YAML instance.""" + with open(projconfigfile, "w") as cf: + ruamelFile_3d.dump(cfg_file_3d, cf) diff --git a/deeplabcut/core/config/validation.py b/deeplabcut/core/config/validation.py new file mode 100644 index 0000000000..aa78845544 --- /dev/null +++ b/deeplabcut/core/config/validation.py @@ -0,0 +1,81 @@ +from collections.abc import Sequence +from typing import Annotated, Any + +import numpy as np +from numpy.typing import NDArray +from pydantic import AfterValidator, BeforeValidator, Field, GetPydanticSchema, InstanceOf + + +def _describe(value: float, name: str | None = None) -> str: + return f"{name} ({value})" if name else f"{value}" + + +def greater_than( + value: float, + threshold: float, + name: str | None = None, + threshold_name: str | None = None, +) -> None: + if value <= threshold: + raise ValueError(f"{_describe(value, name)} must be greater than {_describe(threshold, threshold_name)}") + + +def less_than( + value: float, + threshold: float, + name: str, + threshold_name: str | None = None, +) -> None: + if value >= threshold: + raise ValueError(f"{_describe(value, name)} must be less than {_describe(threshold, threshold_name)}") + + +def unique_values(values: Sequence[Any]) -> Sequence[Any]: + if len(values) != len(set(values)): + raise ValueError("Values must be unique") + return values + + +def validate_crop_bounds( + *, + x1: int | None, + x2: int | None, + y1: int | None, + y2: int | None, +) -> None: + bounds = (x1, x2, y1, y2) + if any(value is None for value in bounds): + if any(value is not None for value in bounds): + raise ValueError("Crop bounds x1, x2, y1, and y2 must either all be set or all be omitted") + return + + less_than(x1, x2, name="x1", threshold_name="x2") + less_than(y1, y2, name="y1", threshold_name="y2") + + +def _coerce_ndarray(v): + if isinstance(v, np.ndarray): + return v + return np.asarray(v, dtype=int) + + +def _bodypart_pair(values: Sequence[Any]) -> list[str]: + if len(values) != 2: + raise ValueError(f"Each bodypart pair must contain exactly two bodyparts, got {len(values)}") + return list(unique_values(values)) + + +Fraction = Annotated[float, Field(ge=0.0, le=1.0)] +UniqueStrList = Annotated[list[str], AfterValidator(unique_values)] +NonNegativeFloat = Annotated[float, Field(ge=0.0)] +NonNegativeInt = Annotated[int, Field(ge=0)] +StrictPositiveInt = Annotated[int, Field(ge=1)] +NDArrayInt = Annotated[ + NDArray, + BeforeValidator(_coerce_ndarray), + GetPydanticSchema( + lambda _s, h: h(InstanceOf[np.ndarray]), + lambda _s, h: h(InstanceOf[np.ndarray]), + ), +] +BodypartPair = Annotated[list[str], AfterValidator(_bodypart_pair)] diff --git a/deeplabcut/core/config/versioning.py b/deeplabcut/core/config/versioning.py new file mode 100644 index 0000000000..56094b123d --- /dev/null +++ b/deeplabcut/core/config/versioning.py @@ -0,0 +1,264 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Configuration migration system for handling version upgrades and downgrades. + +This module provides a versioned migration system that allows configurations +to be upgraded from older versions to newer ones, or downgraded to older formats. +Upgrade migrations are chained together, so any version can be upgraded to the +latest by applying all intermediate migrations in sequence. Downgrade migrations +can be registered for specific version pairs when backward compatibility is needed. +""" + +import copy +import logging +from collections.abc import Callable +from functools import wraps + +logger = logging.getLogger(__name__) + +# Current configuration schema version +# Increment this when making breaking changes to the config structure +CURRENT_CONFIG_VERSION = 0 + + +# Version registry: maps (config_type, from_version, to_version) -> migration function. +# config_type is the class name of the DLCVersionedConfig subclass the migration applies to +# (e.g. "ProjectConfig", "PoseConfig"). Every migration must declare its target type. +_MIGRATIONS: dict[tuple[str, int, int], Callable[[dict], dict]] = {} + + +def _diff_dicts( + before: dict, + after: dict, + path: str = "", + ignore_keys: set[str] | None = None, +) -> list[str]: + """Recursively diff two dicts and return human-readable change descriptions. + + Args: + before: The dict before the migration step. + after: The dict after the migration step. + path: Dot-separated key path for nested context (used in recursion). + ignore_keys: Top-level keys to skip (only applied at the root level). + + Returns: + A list of strings, each describing a single added/removed/updated field. + """ + ignore = ignore_keys or set() + before_keys = set(before.keys()) - ignore + after_keys = set(after.keys()) - ignore + + changes: list[str] = [] + + for key in sorted(before_keys - after_keys): + full = f"{path}.{key}" if path else str(key) + changes.append(f"Removed field '{full}' (was: {before[key]!r})") + + for key in sorted(after_keys - before_keys): + full = f"{path}.{key}" if path else str(key) + changes.append(f"Added field '{full}' = {after[key]!r}") + + for key in sorted(before_keys & after_keys): + old_val, new_val = before[key], after[key] + if old_val == new_val: + continue + full = f"{path}.{key}" if path else str(key) + if isinstance(old_val, dict) and isinstance(new_val, dict): + changes.extend(_diff_dicts(old_val, new_val, path=full)) + else: + changes.append(f"Updated field '{full}': {old_val!r} -> {new_val!r}") + + return changes + + +def _log_field_changes(before: dict, after: dict, from_v: int, to_v: int) -> None: + """Log which fields were added, removed, or changed during a migration step.""" + step = f"v{from_v} -> v{to_v}" + changes = _diff_dicts(before, after, ignore_keys={"config_version"}) + + if changes: + for change in changes: + logger.debug(" [%s] %s", step, change) + else: + logger.debug(" [%s] No field changes", step) + + +def register_migration( + from_version: int, + to_version: int, + config_type: str, +): + """Decorator to register a migration function for a specific config type. + + Every migration must be scoped to a concrete ``DLCVersionedConfig`` subclass. + This keeps ``ProjectConfig`` and ``PoseConfig`` migration chains fully independent. + + Args: + from_version: The source version number (>= 0). + to_version: The target version number (>= 0, from_version ± 1 by convention). + config_type: Class name of the config this migration applies to (e.g. + ``"ProjectConfig"`` or ``"PoseConfig"``). + + Raises: + ValueError: If version numbers are invalid or a migration for the same + (config_type, from_version, to_version) triple is already registered. + + Example:: + + @register_migration(0, 1, config_type="ProjectConfig") + def migrate_project_v0_to_v1(config: dict) -> dict: + config["unique_bodyparts"] = config.pop("uniquebodyparts", []) + return config + """ + if from_version < 0 or to_version < 0: + raise ValueError(f"Version numbers must be non-negative, got ({from_version}, {to_version})") + if from_version == to_version: + raise ValueError(f"from_version and to_version must differ, got ({from_version}, {to_version})") + + def decorator(func: Callable[[dict], dict]) -> Callable[[dict], dict]: + key = (config_type, from_version, to_version) + if key in _MIGRATIONS: + raise ValueError( + f"Duplicate migration registered for {key}. " + f"Existing: {_MIGRATIONS[key].__wrapped__.__qualname__}, " + f"new: {func.__qualname__}" + ) + + @wraps(func) + def wrapper(config: dict) -> dict: + verbose = logger.isEnabledFor(logging.DEBUG) + if verbose: + before = copy.deepcopy(config) + result = func(config.copy()) # Don't mutate caller's dict + result["config_version"] = to_version + if verbose: + _log_field_changes(before, result, from_version, to_version) + return result + + _MIGRATIONS[key] = wrapper + return wrapper + + return decorator + + +def get_config_version(config: dict) -> int: + """Extract the configuration version from a config dict. + + Args: + config: Configuration dictionary + + Returns: + Version number (0 for legacy/unversioned configs) + """ + return config.get("config_version", 0) + + +def migrate_config( + config: dict, + config_type: str, + target_version: int = CURRENT_CONFIG_VERSION, +) -> dict: + """Migrate a configuration to the target version. + + Applies all necessary migrations in sequence to upgrade the config + from its current version to the target version. Only migrations registered + for ``config_type`` are applied. + + Args: + config: Configuration dictionary to migrate. + config_type: Class name of the config being migrated (e.g. ``"ProjectConfig"``). + Only migrations registered for this type are applied. + target_version: Target version to migrate to (default: current). + + Returns: + Migrated configuration dictionary. + + Raises: + ValueError: If migration chain is incomplete or target version is invalid. + """ + current_version = get_config_version(config) + + if current_version == target_version: + return config + + if target_version > CURRENT_CONFIG_VERSION: + raise ValueError(f"Target version {target_version} exceeds current version {CURRENT_CONFIG_VERSION}") + + direction = "upgrade" if target_version > current_version else "downgrade" + logger.info( + "Migrating %s from version %d to %d (%s)", + config_type, + current_version, + target_version, + direction, + ) + + # Chain migrations one step at a time (e.g. 0→1→2 or 3→2→1). + migrated = config + step = 1 if target_version > current_version else -1 + for v in range(current_version, target_version, step): + next_v = v + step + key = (config_type, v, next_v) + if key not in _MIGRATIONS: + raise ValueError( + f"No migration registered for '{config_type}' v{v} -> v{next_v}. " + f"Available migrations: {list(_MIGRATIONS.keys())}" + ) + logger.debug( + "Applying migration %s v%d -> v%d (%s)", + config_type, + v, + next_v, + _MIGRATIONS[key].__wrapped__.__qualname__, + ) + try: + migrated = _MIGRATIONS[key](migrated) + except Exception as exc: + raise type(exc)( + f"Migration for '{config_type}' v{v} -> v{next_v} failed " + f"({_MIGRATIONS[key].__wrapped__.__qualname__}): {exc}" + ) from exc + + logger.info("Migration complete: %s is now at version %d", config_type, target_version) + return migrated + + +# ============================================================================ +# Migration Definitions +# ============================================================================ + + +@register_migration(0, 1, config_type="ProjectConfig") +def migrate_project_v0_to_v1(config: dict) -> dict: + """Migrate ProjectConfig from unversioned/legacy (v0) to v1.""" + # Migration logic goes here, when migrating to v1. + return config + + +@register_migration(1, 0, config_type="ProjectConfig") +def migrate_project_v1_to_v0(config: dict) -> dict: + """Migrate ProjectConfig from v1 back to v0 (legacy format).""" + # Migration logic goes here, when migrating to v1. + return config + + +@register_migration(0, 1, config_type="PoseConfig") +def migrate_pose_v0_to_v1(config: dict) -> dict: + """Migrate PoseConfig from v0 to v1.""" + # Migration logic goes here, when migrating to v1. + return config + + +@register_migration(1, 0, config_type="PoseConfig") +def migrate_pose_v1_to_v0(config: dict) -> dict: + """Migrate PoseConfig from v1 back to v0.""" + # Migration logic goes here, when migrating to v1. + return config diff --git a/deeplabcut/core/conversion_table.py b/deeplabcut/core/conversion_table.py new file mode 100644 index 0000000000..d40ac940c4 --- /dev/null +++ b/deeplabcut/core/conversion_table.py @@ -0,0 +1,80 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Defines conversion tables mapping DeepLabCut project bodyparts to SA bodyparts.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class ConversionTable: + """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) + - All project bodyparts must be valid (defined for the DLC project) + """ + + super_animal: str + project_bodyparts: list[str] + super_animal_bodyparts: list[str] + table: dict[str, str] + + def __post_init__(self): + """Validates the table.""" + self.validate() + + def to_array(self) -> np.ndarray: + """ + Returns: + An array mapping the indices of SuperAnimal bodyparts + + Raises: + ValueError: If the conversion table is misconfigured. + """ + self.validate() + sa_indices = {sa_bpt: i for i, sa_bpt in enumerate(self.super_animal_bodyparts)} + sa_bpt_ordering = [self.table[bpt] for bpt in self.converted_bodyparts()] + return np.array([sa_indices[sa_bpt] for sa_bpt in sa_bpt_ordering]) + + def converted_bodyparts(self) -> list[str]: + """Returns: The project bodyparts included in this ordered""" + return [bpt for bpt in self.project_bodyparts if bpt in self.table] + + def validate(self) -> None: + """ + Raises: + ValueError: If the conversion table is misconfigured. + """ + project_bpts = set(self.project_bodyparts) + sa_bpts = set(self.super_animal_bodyparts) + + mapped_sa = set(self.table.values()) + mapped_project = set(self.table.keys()) + + # check all mapped SuperAnimal bodyparts are in the config + if len(mapped_sa.difference(sa_bpts)) != 0: + extra_bodyparts = set(mapped_sa).difference(sa_bpts) + raise ValueError( + f"Some bodyparts in your mapping are not in the {self.super_animal} " + f"model: {extra_bodyparts}. Available bodyparts are {' '.join(sa_bpts)}" + ) + + # check all given bodyparts are in the project configuration + if len(mapped_project.difference(project_bpts)) != 0: + extra_bodyparts = mapped_project.difference(project_bpts) + raise ValueError( + "Some bodyparts in your mapping are not in your project configuration: " + f"{extra_bodyparts}. Defined bodyparts are {' '.join(project_bpts)}" + ) diff --git a/deeplabcut/core/crossvalutils.py b/deeplabcut/core/crossvalutils.py new file mode 100644 index 0000000000..ca1447c611 --- /dev/null +++ b/deeplabcut/core/crossvalutils.py @@ -0,0 +1,456 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + + +import pickle +import shutil +from collections import defaultdict +from copy import deepcopy +from pathlib import Path + +import networkx as nx +import numpy as np +import pandas as pd +from scipy.spatial import cKDTree +from sklearn.metrics.cluster import contingency_matrix +from tqdm import tqdm + +from deeplabcut.core.inferenceutils import ( + Assembler, + _parse_ground_truth_data, + evaluate_assembly, +) +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions + + +def _set_up_evaluation(data): + params = dict() + params["joint_names"] = data["metadata"]["all_joints_names"] + 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["bpts"] = params["ibpts"] = range(params["num_joints"]) + params["imnames"] = [fn for fn in list(data) if fn != "metadata"] + return params + + +def _form_original_path(path): + p = Path(path) + ext = p.suffix + filename = p.name + return str(p.parent / (filename.split("c")[0] + ext)) + + +def _unsorted_unique(array): + _, inds = np.unique(array, return_index=True) + return np.asarray(array)[np.sort(inds)] + + +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: + query: the query keypoints + ref: the reference keypoints + k: The list of k-th nearest neighbors to return. + + Returns: + an array of shape (len(query), ) containing the index of the closest + reference keypoint for each query keypoint + """ + n_preds = ref.shape[0] + tree = cKDTree(ref) + dist, inds = tree.query(query, k=k) + idx = np.argsort(dist[:, 0]) + neighbors = np.full(len(query), -1, dtype=int) + picked = {tree.n} + for i, ind in enumerate(inds[idx]): + for j in ind: + if j not in picked: + picked.add(j) + neighbors[idx[i]] = j + break + if len(picked) == (n_preds + 1): + break + return neighbors + + +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'.") + + bins = np.linspace(0, 1, n_bins) + hist_left = np.histogram(vals_left, bins=bins)[0] + hist_left = hist_left / hist_left.sum() + hist_right = np.histogram(vals_right, bins=bins)[0] + 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 + else: + sep = np.trapz(np.cumsum(hist_left), tpr) + if max_sensitivity: + threshold = bins[max(1, np.argmax(tpr > 0))] + else: + threshold = bins[np.argmin(1 - np.cumsum(hist_left) + tpr)] + return sep, threshold + + +def _calc_within_between_pafs( + data, + metadata, + per_edge=True, + train_set_only=True, +): + data = deepcopy(data) + train_inds = set(metadata["data"]["trainIndices"]) + graph = data["metadata"]["PAFgraph"] + within_train = defaultdict(list) + within_test = defaultdict(list) + between_train = defaultdict(list) + between_test = defaultdict(list) + for i, (key, dict_) in enumerate(data.items()): + if key == "metadata": + continue + + is_train = i in train_inds + if train_set_only and not is_train: + continue + + df = dict_["groundtruth"][2] + try: + df.drop("single", level="individuals", inplace=True) + except KeyError: + pass + bpts = df.index.get_level_values("bodyparts").unique().to_list() + coords_gt = ( + df.unstack(["individuals", "coords"]) + .reindex(bpts, level="bodyparts") + .to_numpy() + .reshape((len(bpts), -1, 2)) + ) + if np.isnan(coords_gt).all(): + continue + + 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, 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]], strict=False)) + + costs = dict_["prediction"]["costs"] + for k, v in costs.items(): + paf = v["m1"] + mask_within = np.zeros(paf.shape, dtype=bool) + s, t = graph[k] + if s not in lookup or t not in lookup: + continue + lu_s = lookup[s] + lu_t = lookup[t] + common_id = set(lu_s).intersection(lu_t) + for id_ in common_id: + mask_within[lu_s[id_], lu_t[id_]] = True + within_vals = paf[mask_within] + between_vals = paf[~mask_within] + if is_train: + within_train[k].extend(within_vals) + between_train[k].extend(between_vals) + else: + within_test[k].extend(within_vals) + between_test[k].extend(between_vals) + if not per_edge: + within_train = np.concatenate([*within_train.values()]) + within_test = np.concatenate([*within_test.values()]) + between_train = np.concatenate([*between_train.values()]) + between_test = np.concatenate([*between_test.values()]) + return (within_train, within_test), (between_train, between_test) + + +def _benchmark_paf_graphs( + config, + inference_cfg, + data, + paf_inds, + greedy=False, + add_discarded=True, + identity_only=False, + calibration_file="", + oks_sigma=0.1, + margin=0, + symmetric_kpts=None, + split_inds=None, +): + metadata = data.pop("metadata") + multi_bpts_orig = auxfun_multianimal.extractindividualsandbodyparts(config)[2] + multi_bpts = [j for j in metadata["all_joints_names"] if j in multi_bpts_orig] + n_multi = len(multi_bpts) + data_ = {"metadata": metadata} + for k, v in data.items(): + data_[k] = v["prediction"] + ass = Assembler( + data_, + max_n_individuals=inference_cfg["topktoretain"], + n_multibodyparts=n_multi, + greedy=greedy, + pcutoff=inference_cfg.get("pcutoff", 0.1), + min_affinity=inference_cfg.get("pafthreshold", 0.1), + add_discarded=add_discarded, + identity_only=identity_only, + ) + if calibration_file: + ass.calibrate(calibration_file) + + 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 + mask_multi = idx.get_level_values("individuals") != "single" + if not mask_multi.all(): + idx = idx.drop("single", level="individuals") + individuals = idx.get_level_values("individuals").unique() + n_individuals = len(individuals) + map_ = dict(zip(individuals, range(n_individuals), strict=False)) + + # Form ground truth beforehand + ground_truth = [] + for i, imname in enumerate(image_paths): + temp = data[imname]["groundtruth"][2].reindex(multi_bpts, level="bodyparts") + ground_truth.append(temp.to_numpy().reshape((-1, 2))) + ground_truth = np.stack(ground_truth) + temp = np.ones((*ground_truth.shape[:2], 3)) + temp[..., :2] = ground_truth + temp = temp.reshape((temp.shape[0], n_individuals, -1, 3)) + ass_true_dict = _parse_ground_truth_data(temp) + ids = np.vectorize(map_.get)(idx.get_level_values("individuals").to_numpy()) + ground_truth = np.insert(ground_truth, 2, ids, axis=2) + + # Assemble animals on the full set of detections + paf_inds = sorted(paf_inds, key=len) + n_graphs = len(paf_inds) + all_scores = [] + all_metrics = [] + all_assemblies = [] + for j, paf in enumerate(paf_inds, start=1): + print(f"Graph {j}|{n_graphs}") + ass.paf_inds = paf + ass.assemble() + all_assemblies.append((ass.assemblies, ass.unique, ass.metadata["imnames"])) + if split_inds is not None: + oks = [] + + # 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} + + oks.append( + evaluate_assembly( + ass_pred, + ass_gt, + oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + greedy_matching=inference_cfg.get("greedy_oks", False), + ) + ) + else: + oks = evaluate_assembly( + ass.assemblies, + ass_true_dict, + oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + greedy_matching=inference_cfg.get("greedy_oks", False), + ) + all_metrics.append(oks) + scores = np.full((len(image_paths), 2), np.nan) + for i, imname in enumerate(tqdm(image_paths)): + gt = ground_truth[i] + gt = gt[~np.isnan(gt).any(axis=1)] + if len(np.unique(gt[:, 2])) < 2: # Only consider frames with 2+ animals + continue + + # Count the number of unassembled bodyparts + n_dets = len(gt) + animals = ass.assemblies.get(i) + if animals is None: + 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)] + hyp = np.concatenate(animals) + hyp = hyp[~np.isnan(hyp).any(axis=1)] + scores[i, 0] = max(0, (n_dets - hyp.shape[0]) / n_dets) + neighbors = find_closest_neighbors(gt[:, :2], hyp[:, :2]) + valid = neighbors != -1 + id_gt = gt[valid, 2] + id_hyp = hyp[neighbors[valid], -1] + mat = contingency_matrix(id_gt, id_hyp) + purity = mat.max(axis=0).sum() / mat.sum() + scores[i, 1] = purity + all_scores.append((scores, paf)) + + dfs = [] + for score, inds in all_scores: + df = pd.DataFrame(score, columns=["miss", "purity"]) + df["ngraph"] = len(inds) + dfs.append(df) + big_df = pd.concat(dfs) + group = big_df.groupby("ngraph") + return (all_scores, group.agg(["mean", "std"]).T, all_metrics, all_assemblies) + + +def _get_n_best_paf_graphs( + data, + metadata, + full_graph, + n_graphs=10, + root=None, + which="best", + ignore_inds=None, + metric="auc", +): + if which not in ("best", "worst"): + raise ValueError('`which` must be either "best" or "worst"') + + (within_train, _), (between_train, _) = _calc_within_between_pafs( + data, + metadata, + train_set_only=True, + ) + # Handle unlabeled bodyparts... + existing_edges = set(k for k, v in within_train.items() if v) + if ignore_inds is not None: + existing_edges = existing_edges.difference(ignore_inds) + existing_edges = list(existing_edges) + + 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), strict=False))) + + 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, strict=False): + if np.isfinite(score): + G.add_edge(*full_graph[edge], weight=score) + if which == "best": + order = np.asarray(existing_edges)[np.argsort(scores)[::-1]] + if root is None: + root = [] + for edge in nx.maximum_spanning_edges(G, data=False): + root.append(full_graph.index(sorted(edge))) + else: + order = np.asarray(existing_edges)[np.argsort(scores)] + if root is None: + root = [] + for edge in nx.minimum_spanning_edges(G, data=False): + root.append(full_graph.index(sorted(edge))) + + n_edges = len(existing_edges) - len(root) + lengths = np.linspace(0, n_edges, min(n_graphs, n_edges + 1), dtype=int)[1:] + order = order[np.isin(order, root, invert=True)] + paf_inds = [root] + for length in lengths: + paf_inds.append(root + list(order[:length])) + return paf_inds, dict(zip(existing_edges, scores, strict=False)) + + +def cross_validate_paf_graphs( + config, + inference_config, + full_data_file, + metadata_file, + output_name="", + pcutoff=0.1, + oks_sigma=0.1, + margin=0, + greedy=False, + add_discarded=True, + calibrate=False, + overwrite_config=True, + n_graphs=10, + paf_inds=None, + symmetric_kpts=None, +): + cfg = auxiliaryfunctions.read_config(config) + inf_cfg = auxiliaryfunctions.read_plainconfig(inference_config) + inf_cfg_temp = inf_cfg.copy() + inf_cfg_temp["pcutoff"] = pcutoff + + with Path(full_data_file).open("rb") as file: + data = pickle.load(file) + with Path(metadata_file).open("rb") as file: + metadata = pickle.load(file) + + params = _set_up_evaluation(data) + to_ignore = auxfun_multianimal.filter_unwanted_paf_connections(cfg, params["paf_graph"]) + best_graphs = _get_n_best_paf_graphs( + data, + metadata, + params["paf_graph"], + ignore_inds=to_ignore, + n_graphs=n_graphs, + ) + paf_scores = best_graphs[1] + if paf_inds is None: + paf_inds = best_graphs[0] + + if calibrate: + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) + calibration_file = str( + Path(cfg["project_path"]) / str(trainingsetfolder) / ("CollectedData_" + cfg["scorer"] + ".h5") + ) + else: + calibration_file = "" + + results = _benchmark_paf_graphs( + cfg, + inf_cfg_temp, + data, + paf_inds, + greedy, + add_discarded, + oks_sigma=oks_sigma, + margin=margin, + symmetric_kpts=symmetric_kpts, + calibration_file=calibration_file, + split_inds=[ + metadata["data"]["trainIndices"], + metadata["data"]["testIndices"], + ], + ) + # Select optimal PAF graph + df = results[1] + size_opt = np.argmax((1 - df.loc["miss", "mean"]) * df.loc["purity", "mean"]) + pose_config = inference_config.replace("inference_cfg", "pose_cfg") + 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]}) + if output_name: + with Path(output_name).open("wb") as file: + pickle.dump([results], file) + return results[:3], paf_scores, results[3][size_opt] + + +# Backwards compatibility +_find_closest_neighbors = find_closest_neighbors diff --git a/deeplabcut/core/debug/__init__.py b/deeplabcut/core/debug/__init__.py new file mode 100644 index 0000000000..bbc583fe09 --- /dev/null +++ b/deeplabcut/core/debug/__init__.py @@ -0,0 +1,46 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from collections.abc import Sequence + +from .debug_logger import ( + DLC_ALL_LIBS_SPECS, + DebugSection, + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + RecordedLog, + build_debug_report, + collect_debug_sections, + collect_executable_summary, + collect_version_summary, + format_debug_report, + get_debug_recorder, + install_debug_recorder, + log_timing, +) + +__all__: Sequence[str] = ( + "DLC_ALL_LIBS_SPECS", + "ExecutableSpec", + "DebugSection", + "InMemoryDebugRecorder", + "LibrarySpec", + "RecordedLog", + "build_debug_report", + "collect_debug_sections", + "collect_executable_summary", + "collect_version_summary", + "format_debug_report", + "get_debug_recorder", + "install_debug_recorder", + "log_timing", +) diff --git a/deeplabcut/core/debug/_debug_utils.py b/deeplabcut/core/debug/_debug_utils.py new file mode 100644 index 0000000000..a1f588708c --- /dev/null +++ b/deeplabcut/core/debug/_debug_utils.py @@ -0,0 +1,87 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from __future__ import annotations + +import os +import shutil +import subprocess +from collections.abc import Sequence +from pathlib import Path + + +def _env_flag(name: str, default: bool = False) -> bool: + """Parse a boolean environment variable. + + Accepted truthy values: + 1, true, yes, on + + Accepted falsy values: + 0, false, no, off + """ + value = os.getenv(name) + if value is None: + return default + + value = value.strip().lower() + if value in {"1", "true", "yes", "on"}: + return True + if value in {"0", "false", "no", "off"}: + return False + return default + + +def _env_optional_float(name: str, default: float | None = None) -> float | None: + """Parse an optional float environment variable. + + Empty strings / unset values return ``default``. + Invalid values also fall back to ``default``. + """ + value = os.getenv(name) + if value is None: + return default + + value = value.strip() + if not value: + return default + + try: + return float(value) + except ValueError: + return default + + +def _which(command: str) -> str: + try: + resolved = shutil.which(command) + return str(Path(resolved).absolute()) if resolved else "not-found" + except Exception: + return "not-found" + + +def _command_version(command: str, version_args: Sequence[str] = ("-version",)) -> str: + try: + completed = subprocess.run( + [command, *version_args], + check=False, + capture_output=True, + text=True, + timeout=3, + ) + except Exception: + return "unavailable" + + text = (completed.stdout or completed.stderr or "").strip() + if not text: + return "unavailable" + + first_line = text.splitlines()[0].strip() + return first_line or "unavailable" diff --git a/deeplabcut/core/debug/debug_logger.py b/deeplabcut/core/debug/debug_logger.py new file mode 100644 index 0000000000..8e531db54b --- /dev/null +++ b/deeplabcut/core/debug/debug_logger.py @@ -0,0 +1,591 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from __future__ import annotations + +import logging +import platform +import sys +import threading +import traceback +from collections import deque +from collections.abc import Iterable +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +from importlib import metadata +from pathlib import Path +from time import perf_counter_ns + +from ._debug_utils import ( + _command_version, + _env_flag, + _env_optional_float, + _which, +) + +_DEBUG_HANDLER_ATTR = "_dlc_debug_recorder" +LOG_QUEUE_MAXLEN = 1000 + +# NOTE @C-Achard 2026-05-13: we may want to centralize env vars in a config/settings module in the future +DLC_LOG_TIMING = _env_flag("DLC_LOG_TIMING", default=False) +DLC_LOG_TIMING_THRESHOLD_MS = _env_optional_float("DLC_LOG_TIMING_THRESHOLD_MS", default=None) + + +def reload_debug_settings_from_env() -> None: + """Reload debug/timing settings from environment variables.""" + global DLC_LOG_TIMING, DLC_LOG_TIMING_THRESHOLD_MS + + DLC_LOG_TIMING = _env_flag("DLC_LOG_TIMING", default=False) + DLC_LOG_TIMING_THRESHOLD_MS = _env_optional_float( + "DLC_LOG_TIMING_THRESHOLD_MS", + default=None, + ) + + +@contextmanager +def log_timing( + logger: logging.Logger, + label: str, + *, + level: int = logging.DEBUG, + threshold_ms: float | None = None, +): + """Lightweight scoped timer for debug instrumentation. + + Uses perf_counter_ns() for monotonic timing. + Logs only if logger is enabled for the requested level. + Optionally suppresses tiny timings below ``threshold_ms``. + """ + if not logger.isEnabledFor(level) or not DLC_LOG_TIMING: + yield + return + + effective_threshold_ms = threshold_ms if threshold_ms is not None else DLC_LOG_TIMING_THRESHOLD_MS + t0 = perf_counter_ns() + try: + yield + finally: + dt_ms = (perf_counter_ns() - t0) / 1e6 + if effective_threshold_ms is None or dt_ms >= effective_threshold_ms: + logger.log(level, "%s took %.3f ms", label, dt_ms) + + +@dataclass(frozen=True) +class RecordedLog: + created: float + level: str + logger_name: str + message: str + exc_text: str | None = None + + +class InMemoryDebugRecorder(logging.Handler): + """Lightweight, fail-open in-memory log recorder. + + Safety properties: + - bounded memory via deque(maxlen=...) + - no file/network I/O + - swallow-all-errors in emit() + - does not log from inside itself + - stores only small text snapshots + """ + + def __init__(self, *, capacity: int = LOG_QUEUE_MAXLEN, level: int = logging.DEBUG): + super().__init__(level=level) + self._records: deque[RecordedLog] = deque(maxlen=max(1, int(capacity))) + self._lock = threading.Lock() + self._dropped = 0 + + @property + def dropped_count(self) -> int: + with self._lock: + return self._dropped + + def emit(self, record: logging.LogRecord) -> None: + try: + # Never call logging from here. + # Never inspect application objects. + msg = self._safe_message(record) + exc_text = self._safe_exception_text(record) + + snap = RecordedLog( + created=float(getattr(record, "created", 0.0) or 0.0), + level=str(getattr(record, "levelname", "UNKNOWN")), + logger_name=str(getattr(record, "name", "")), + message=msg, + exc_text=exc_text, + ) + + with self._lock: + self._records.append(snap) + + except Exception: + # Fail open: never let diagnostics interfere with runtime behavior. + try: + self._dropped += 1 + except Exception: + pass + + def clear(self) -> None: + try: + with self._lock: + self._records.clear() + self._dropped = 0 + except Exception: + pass + + def snapshot(self) -> list[RecordedLog]: + try: + with self._lock: + return list(self._records) + except Exception: + return [] + + def render_text(self, *, limit: int = 200) -> str: + lines: list[str] = [] + try: + records = self.snapshot()[-max(1, int(limit)) :] + if not records: + if self._dropped: + return f"[debug-recorder] no captured logs, {self._dropped} internal failures" + return "" + + base = records[0].created + for rec in records: + ts = datetime.fromtimestamp(rec.created).strftime("%H:%M:%S.%f")[:-3] + if DLC_LOG_TIMING: + rel_ms = (rec.created - base) * 1000.0 + lines.append(f"{ts} (+{rel_ms:8.1f} ms) | {rec.level:<8} | {rec.logger_name} | {rec.message}") + else: + lines.append(f"{ts} | {rec.level:<8} | {rec.logger_name} | {rec.message}") + if rec.exc_text: + lines.append(rec.exc_text.rstrip()) + + if self._dropped: + lines.append(f"[debug-recorder] dropped internal failures: {self._dropped}") + except Exception: + return "[debug-recorder] failed to render logs" + return "\n".join(lines) + + @staticmethod + def _safe_message(record: logging.LogRecord) -> str: + try: + return record.getMessage() + except Exception: + try: + return str(record.msg) + except Exception: + return "" + + @staticmethod + def _safe_exception_text(record: logging.LogRecord) -> str | None: + try: + if not record.exc_info: + return None + return "".join(traceback.format_exception(*record.exc_info)) + except Exception: + return "" + + +@dataclass(frozen=True) +class DebugSection: + title: str + items: dict[str, str] + + +def install_debug_recorder( + *, + logger_name: str = "deeplabcut", + capacity: int = LOG_QUEUE_MAXLEN, + handler_level: int = logging.INFO, + ensure_logger_level: int | None = None, +) -> InMemoryDebugRecorder: + """Attach a single in-memory recorder to the requested logger namespace. + + Idempotent: repeated calls return the same recorder. + + Args: + logger_name (str): Logger namespace to attach the recorder to. + capacity (int): Maximum number of captured records. By default, uses + LOG_QUEUE_MAXLEN. + handler_level (int): Minimum level stored by the recorder itself. + ensure_logger_level (int | None): Controls whether to adjust the target logger + level. + + - None: never modify the logger level + - int: lower the logger only if its effective level is more restrictive + + Returns: + InMemoryDebugRecorder: The attached recorder. + """ + root_logger = logging.getLogger(logger_name) + + existing = getattr(root_logger, _DEBUG_HANDLER_ATTR, None) + if isinstance(existing, InMemoryDebugRecorder): + return existing + + recorder = InMemoryDebugRecorder(capacity=capacity, level=handler_level) + recorder.set_name("deeplabcut-debug-recorder") + + # Important: + # - attach only to a DLC-owned logger namespace, not the global root logger + # - keep propagation unchanged + # - logger level adjustment, if any, is handled below; "auto" initializes + # an unset logger to ``handler_level`` rather than forcing DEBUG + root_logger.addHandler(recorder) + + if isinstance(ensure_logger_level, int): + # Only lower verbosity if explicitly requested. + if root_logger.getEffectiveLevel() > ensure_logger_level: + root_logger.setLevel(ensure_logger_level) + + setattr(root_logger, _DEBUG_HANDLER_ATTR, recorder) + return recorder + + +def get_debug_recorder(*, logger_name: str = "deeplabcut") -> InMemoryDebugRecorder | None: + logger = logging.getLogger(logger_name) + recorder = getattr(logger, _DEBUG_HANDLER_ATTR, None) + return recorder if isinstance(recorder, InMemoryDebugRecorder) else None + + +# -------------------------- +# Environment / version info +# -------------------------- + + +@dataclass(frozen=True) +class LibrarySpec: + """Small description of a library to report.""" + + key: str + dist_name: str | None = None + module_name: str | None = None + prefer_module_version: bool = False + + def resolved_dist_name(self) -> str: + return self.dist_name or self.key + + def resolved_module_name(self) -> str: + return self.module_name or self.key + + +DLC_CORE_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("deeplabcut"), + LibrarySpec("torch"), + LibrarySpec("torchvision"), + LibrarySpec("numpy"), + LibrarySpec("pandas"), + LibrarySpec("scipy"), + LibrarySpec("h5py"), + LibrarySpec("tables"), + LibrarySpec("opencv-python", dist_name="opencv-python", module_name="cv2", prefer_module_version=True), +) +DLC_GUI_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("PySide6"), + LibrarySpec("shiboken6"), + LibrarySpec("qtpy", dist_name="QtPy"), + LibrarySpec("qdarkstyle"), + LibrarySpec("napari"), + LibrarySpec("napari-deeplabcut", dist_name="napari-deeplabcut", module_name="napari_deeplabcut"), +) +DLC_TF_LIBS: tuple[LibrarySpec, ...] = ( + LibrarySpec("tensorflow"), + LibrarySpec("tf_keras", dist_name="tf-keras"), + LibrarySpec("tensorpack"), + LibrarySpec("tf_slim", dist_name="tf-slim"), +) +DLC_ALL_LIBS_SPECS: tuple[LibrarySpec, ...] = DLC_CORE_LIBS + DLC_GUI_LIBS + DLC_TF_LIBS + + +def _normalize_library_specs( + libraries: Iterable[LibrarySpec | str] | None, +) -> tuple[LibrarySpec, ...]: + if libraries is None: + return DLC_ALL_LIBS_SPECS + + normalized: list[LibrarySpec] = [] + for item in libraries: + if isinstance(item, LibrarySpec): + normalized.append(item) + else: + normalized.append(LibrarySpec(str(item))) + return tuple(normalized) + + +def _version(dist_name: str) -> str: + try: + return metadata.version(dist_name) + except Exception: + return "not-installed" + + +def _module_path(module_name: str) -> str: + try: + mod = __import__(module_name) + p = getattr(mod, "__file__", None) + return str(Path(p).absolute()) if p else "unknown" + except Exception: + return "unknown" + + +def _safe_tail(pathlike: object) -> str: + """Redact user-specific absolute paths. + + Keeps only the last 2 path components when possible. + """ + try: + p = Path(str(pathlike)) + parts = p.parts + if len(parts) >= 2: + return str(Path(*parts[-2:]).as_posix()) + return str(p.as_posix()) + except Exception: + return str(pathlike) + + +def _module_version(module_name: str) -> str: + try: + mod = __import__(module_name) + version = getattr(mod, "__version__", None) + if version: + return str(version) + return "unknown" + except Exception: + return "not-installed" + + +def collect_version_summary( + *, + libraries: Iterable[LibrarySpec | str] | None = None, + include_module_paths: bool = False, +) -> dict[str, str]: + specs = _normalize_library_specs(libraries) + summary: dict[str, str] = {} + + for spec in specs: + key = spec.key + module_name = spec.resolved_module_name() + + if spec.prefer_module_version: + version = _module_version(module_name) + if version in {"not-installed", "unknown"}: + version = _version(spec.resolved_dist_name()) + else: + version = _version(spec.resolved_dist_name()) + + summary[key] = version + + if include_module_paths: + summary[f"{key}_module_path"] = _safe_tail(_module_path(module_name)) + + return summary + + +@dataclass(frozen=True) +class ExecutableSpec: + """Small description of an external executable to report. + + Args: + key (str): Label used in the output report. + command (str | None): Executable name or absolute path to resolve. + version_args (tuple[str, ...]): Arguments used to query the executable version. + Defaults to ("-version",). + """ + + key: str + command: str | None = None + version_args: tuple[str, ...] = ("-version",) + + def resolved_command(self) -> str: + return self.command or self.key + + +DEFAULT_EXECUTABLE_SPECS: tuple[ExecutableSpec, ...] = (ExecutableSpec("ffmpeg"),) + + +def _normalize_executable_specs( + executables: Iterable[ExecutableSpec | str] | None, +) -> tuple[ExecutableSpec, ...]: + if executables is None: + return DEFAULT_EXECUTABLE_SPECS + + normalized: list[ExecutableSpec] = [] + for item in executables: + if isinstance(item, ExecutableSpec): + normalized.append(item) + else: + normalized.append(ExecutableSpec(str(item))) + return tuple(normalized) + + +def collect_executable_summary( + *, + executables: Iterable[ExecutableSpec | str] | None = None, + include_paths: bool = True, +) -> dict[str, str]: + specs = _normalize_executable_specs(executables) + summary: dict[str, str] = {} + + for spec in specs: + key = spec.key + command = spec.resolved_command() + summary[key] = _command_version(command, spec.version_args) + if include_paths: + summary[f"{key}_path"] = _safe_tail(_which(command)) + + return summary + + +# -------------------------- +# Report formatting +# -------------------------- + + +def format_debug_report( + *, + sections: Iterable[DebugSection], + logs_text: str, +) -> str: + lines: list[str] = [] + + for section in sections: + lines.append(f"## {section.title}") + if section.items: + for k, v in section.items.items(): + lines.append(f"- {k}: {v}") + else: + lines.append("- ") + lines.append("") + + lines.append("## Recent logs") + lines.append("```text") + lines.append(logs_text or "") + lines.append("```") + + return "\n".join(lines) + + +def build_debug_report( + *, + recorder: InMemoryDebugRecorder | None, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, +) -> str: + logs_text = recorder.render_text(limit=log_limit) if recorder is not None else "" + + sections = collect_debug_sections( + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + ) + + return format_debug_report( + sections=sections, + logs_text=logs_text, + ) + + +def collect_runtime_summary() -> dict[str, str]: + return { + "python": sys.version.replace("\n", " "), + "platform": platform.platform(), + "executable": _safe_tail(sys.executable), + } + + +def _section_has_useful_values(items: dict[str, str]) -> bool: + for value in items.values(): + if value not in {"not-installed", "unknown", "not-found", "unavailable"}: + return True + return False + + +def collect_debug_sections( + *, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, +) -> list[DebugSection]: + sections: list[DebugSection] = [] + + # Always include the runtime section first + sections.append( + DebugSection( + title="Runtime", + items=collect_runtime_summary(), + ) + ) + + # Default grouped report using your built-in constants + if libraries is None: + sections.append( + DebugSection( + title="DeepLabCut core libraries", + items=collect_version_summary( + libraries=DLC_CORE_LIBS, + include_module_paths=include_module_paths, + ), + ) + ) + + sections.append( + DebugSection( + title="GUI libraries", + items=collect_version_summary( + libraries=DLC_GUI_LIBS, + include_module_paths=include_module_paths, + ), + ) + ) + + tf_items = collect_version_summary( + libraries=DLC_TF_LIBS, + include_module_paths=include_module_paths, + ) + if tf_items and _section_has_useful_values(tf_items): + sections.append( + DebugSection( + title="TensorFlow libraries", + items=tf_items, + ) + ) + else: + # Custom input + sections.append( + DebugSection( + title="Libraries", + items=collect_version_summary( + libraries=libraries, + include_module_paths=include_module_paths, + ), + ) + ) + + exec_items = collect_executable_summary( + executables=executables, + include_paths=include_executable_paths, + ) + if exec_items: # report if unavailable + sections.append( + DebugSection( + title="External tools", + items=exec_items, + ), + ) + + return sections diff --git a/deeplabcut/core/deprecation.py b/deeplabcut/core/deprecation.py new file mode 100644 index 0000000000..ed0bd0c7d1 --- /dev/null +++ b/deeplabcut/core/deprecation.py @@ -0,0 +1,203 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +import functools +import inspect +import warnings +from collections.abc import Callable +from typing import Literal, ParamSpec, TypeVar + +from packaging.version import InvalidVersion, Version +from pydantic import BaseModel, ConfigDict, field_validator, model_validator + +P = ParamSpec("P") +R = TypeVar("R") + + +class DLCDeprecationWarning(DeprecationWarning): + """Project-specific deprecation warning. Helps with filtering.""" + + +class DeprecationInfo(BaseModel): + model_config = ConfigDict( + frozen=True, + arbitrary_types_allowed=True, + ) + + kind: Literal["callable", "parameter"] + target: str + replacement: str | None = None + + since: Version | None = None + removed_in: Version | None = None + + old_parameter: str | None = None + new_parameter: str | None = None + + @field_validator("since", "removed_in", mode="before") + @classmethod + def _parse_version(cls, value): + if value is None or isinstance(value, Version): + return value + try: + return Version(value) + except InvalidVersion as e: + raise ValueError(f"Invalid version: {value!r}") from e + + @model_validator(mode="after") + def _validate_version_order(self) -> DeprecationInfo: + if self.since and self.removed_in and self.removed_in <= self.since: + raise ValueError(f"'removed_in' ({self.removed_in}) must be greater than 'since' ({self.since}).") + return self + + def format_message(self) -> str: + if self.kind == "callable": + parts = [f"{self.target} is deprecated"] + if self.since: + parts[0] += f" since {self.since}" + if self.replacement: + parts.append(f"Use {self.replacement} instead.") + if self.removed_in: + parts.append(f"It will be removed in {self.removed_in}.") + return " ".join(parts) + + if self.kind == "parameter": + return ( + f"Parameter '{self.old_parameter}' of {self.target} is deprecated" + + (f" since {self.since}" if self.since else "") + + f"; use '{self.new_parameter}' instead." + ) + + raise ValueError(f"Unknown deprecation kind: {self.kind}") + + +def deprecated( + *, + replacement: str | None = None, + since: str | None = None, + removed_in: str | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """Mark a function as deprecated. + + Args: + replacement: Fully-qualified name of the replacement callable, e.g. + ``"deeplabcut.utils.auxfun_videos.list_videos_in_folder"``. + since: Version in which the function was deprecated. + removed_in: Version in which the function will be removed. + """ + + def decorator(fn: Callable[P, R]) -> Callable[P, R]: + info = DeprecationInfo( + kind="callable", + target=fn.__qualname__, + replacement=replacement, + since=since, + removed_in=removed_in, + ) + message = info.format_message() + + @functools.wraps(fn) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + warnings.warn(message, DLCDeprecationWarning, stacklevel=2) + return fn(*args, **kwargs) + + wrapper.__doc__ = f"Deprecated. {message}\n\n" + (fn.__doc__ or "") + wrapper.__deprecated_info__ = info + return wrapper + + return decorator + + +def renamed_parameter( + *, + old: str, + new: str, + since: str | None = None, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """Support a renamed keyword argument while warning callers to update. + + Args: + old: The old parameter name that callers may still pass. + new: The current parameter name the function actually accepts. + since: Version when the rename happened. + + Rules: + - ``new`` must be the name used in the function signature and all + internal call-sites. ``old`` must **not** appear in the signature. + - Do **not** chain renames. If ``A`` was renamed to ``B`` and ``B`` + is later renamed to ``C``, replace the ``A→B`` decorator with + ``A→C`` directly rather than stacking a second decorator. + Example: + @renamed_parameter(old="A", new="C", since="12.4.0") + @renamed_parameter(old="B", new="C", since="13.0.0") + def func(*, C: int): + print(f"C={C}") + - Multiple independent renames on the same function (e.g. + ``batchsize→batch_size`` *and* ``videotype→video_extensions``) are fine + as long as they do not form a chain. + - This decorator only intercepts **keyword** arguments. Positional + arguments are passed through unchanged; renaming a parameter that + callers commonly pass positionally will not be caught. + """ + + def decorator(fn: Callable[P, R]) -> Callable[P, R]: + sig = inspect.signature(fn) + + # Guard: disallow chaining renames (A→B stacked on top of B→C). + existing = getattr(fn, "__deprecated_params__", ()) + for prev in existing: + if prev.old_parameter == new: + raise ValueError( + f"@renamed_parameter: chaining renames is not allowed. " + f"'{old}' → '{new}' would chain with the existing " + f"'{prev.old_parameter}' → '{prev.new_parameter}' rename " + f"on {fn.__qualname__}. " + f"Use '{old}' → '{prev.new_parameter}' directly instead." + ) + + # Guard: 'new' must actually exist in the function's signature. + if new not in sig.parameters: + raise ValueError( + f"@renamed_parameter: '{new}' is not a parameter of " + f"{fn.__qualname__}. " + f"Available parameters: {list(sig.parameters)}" + ) + + # Guard: 'old' must NOT exist in the signature. + if old in sig.parameters: + raise ValueError( + f"@renamed_parameter: '{old}' is still a parameter of " + f"{fn.__qualname__}. Use either old name or new name: '{new}'." + ) + + info = DeprecationInfo( + kind="parameter", + target=fn.__qualname__, + since=since, + old_parameter=old, + new_parameter=new, + ) + message = info.format_message() + + @functools.wraps(fn) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + if old in kwargs: + if new in kwargs: + raise TypeError(f"{fn.__qualname__} received both '{old}' and '{new}'. Use only '{new}'.") + warnings.warn(message, DLCDeprecationWarning, stacklevel=2) + kwargs[new] = kwargs.pop(old) + return fn(*args, **kwargs) + + wrapper.__deprecated_params__ = (*existing, info) + return wrapper + + return decorator diff --git a/deeplabcut/core/engine.py b/deeplabcut/core/engine.py new file mode 100644 index 0000000000..1f7a51d60b --- /dev/null +++ b/deeplabcut/core/engine.py @@ -0,0 +1,50 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Defines the deep learning frameworks available.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +@dataclass(frozen=True) +class EngineDataMixin: + aliases: tuple[str] + model_folder_name: str + pose_cfg_name: str + results_folder_name: str + + +class Engine(EngineDataMixin, Enum): + PYTORCH = ( + ("pytorch", "torch"), + "dlc-models-pytorch", + "pytorch_config.yaml", + "evaluation-results-pytorch", + ) + TF = ( + ("tensorflow", "tf"), + "dlc-models", + "pose_cfg.yaml", + "evaluation-results", + ) + + @classmethod + def _missing_(cls, value): + if isinstance(value, str): + for member in cls: + if value.lower() in member.aliases: + return member + return None + + def __repr__(self) -> str: + return f"Engine.{self.name}" diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py new file mode 100644 index 0000000000..b31bd85814 --- /dev/null +++ b/deeplabcut/core/inferenceutils.py @@ -0,0 +1,1268 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +import heapq +import itertools +import multiprocessing +import operator +import pickle +import warnings +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass +from math import erf, sqrt +from pathlib import Path +from typing import Any + +import networkx as nx +import numpy as np +import pandas as pd +from scipy.optimize import linear_sum_assignment +from scipy.spatial import cKDTree +from scipy.spatial.distance import cdist, pdist +from scipy.special import softmax +from scipy.stats import chi2, gaussian_kde +from tqdm import tqdm + + +def _conv_square_to_condensed_indices(ind_row, ind_col, n): + if ind_row == ind_col: + raise ValueError("There are no diagonal elements in condensed matrices.") + + if ind_row < ind_col: + ind_row, ind_col = ind_col, ind_row + return n * ind_col - ind_col * (ind_col + 1) // 2 + ind_row - 1 - ind_col + + +Position = tuple[float, float] + + +@dataclass(frozen=True) +class Joint: + pos: Position + confidence: float = 1.0 + label: int = None + idx: int = None + group: int = -1 + + +class Link: + def __init__(self, j1, j2, affinity=1): + self.j1 = j1 + self.j2 = j2 + self.affinity = affinity + 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}" + + @property + def confidence(self): + return self.j1.confidence * self.j2.confidence + + @property + def idx(self): + return self.j1.idx, self.j2.idx + + @property + def length(self): + return self._length + + @length.setter + def length(self, length): + self._length = length + + def to_vector(self): + return [*self.j1.pos, *self.j2.pos] + + +class Assembly: + def __init__(self, size): + self.data = np.full((size, 4), np.nan) + self.confidence = 0 # 0 by default, overwritten otherwise with `add_joint` + self._affinity = 0 + self._links = [] + self._visible = set() + self._idx = set() + self._dict = dict() + + def __len__(self): + return len(self._visible) + + def __contains__(self, assembly): + return bool(self._visible.intersection(assembly._visible)) + + def __add__(self, other): + if other in self: + raise ValueError("Assemblies contain shared joints.") + + assembly = Assembly(self.data.shape[0]) + for link in self._links + other._links: + assembly.add_link(link) + return assembly + + @classmethod + def from_array(cls, array): + n_bpts, n_cols = array.shape + + # if a single coordinate is NaN for a bodypart, set all to NaN + array[np.isnan(array).any(axis=-1)] = np.nan + + ass = cls(size=n_bpts) + ass.data[:, :n_cols] = array + visible = np.flatnonzero(~np.isnan(array).any(axis=1)) + if n_cols < 3: # Only xy coordinates are being set + ass.data[visible, 2] = 1 # Set detection confidence to 1 + ass._visible.update(visible) + return ass + + @property + def xy(self): + return self.data[:, :2] + + @property + def extent(self): + bbox = np.empty(4) + bbox[:2] = np.nanmin(self.xy, axis=0) + bbox[2:] = np.nanmax(self.xy, axis=0) + return bbox + + @property + def area(self): + x1, y1, x2, y2 = self.extent + return (x2 - x1) * (y2 - y1) + + @property + def confidence(self): + return np.nanmean(self.data[:, 2]) + + @confidence.setter + def confidence(self, confidence): + self.data[:, 2] = confidence + + @property + def soft_identity(self): + data = self.data[~np.isnan(self.data).any(axis=1)] + 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, strict=False)) + + @property + def affinity(self): + n_links = self.n_links + if not n_links: + return 0 + return self._affinity / n_links + + @property + def n_links(self): + return len(self._links) + + def intersection_with(self, other): + x11, y11, x21, y21 = self.extent + x12, y12, x22, y22 = other.extent + x1 = max(x11, x12) + y1 = max(y11, y12) + x2 = min(x21, x22) + y2 = min(y21, y22) + if x2 < x1 or y2 < y1: + return 0 + ll = np.array([x1, y1]) + ur = np.array([x2, y2]) + xy1 = self.xy[~np.isnan(self.xy).any(axis=1)] + xy2 = other.xy[~np.isnan(other.xy).any(axis=1)] + in1 = np.all((xy1 >= ll) & (xy1 <= ur), axis=1).sum() + in2 = np.all((xy2 >= ll) & (xy2 <= ur), axis=1).sum() + return min(in1 / len(self), in2 / len(other)) + + def add_joint(self, joint): + if joint.label in self._visible or joint.label is None: + return False + self.data[joint.label] = *joint.pos, joint.confidence, joint.group + self._visible.add(joint.label) + self._idx.add(joint.idx) + return True + + def remove_joint(self, joint): + if joint.label not in self._visible: + return False + self.data[joint.label] = np.nan + self._visible.remove(joint.label) + self._idx.remove(joint.idx) + return True + + def add_link(self, link, store_dict=False): + if store_dict: + # Selective copy; deepcopy is >5x slower + self._dict = { + "data": self.data.copy(), + "_affinity": self._affinity, + "_links": self._links.copy(), + "_visible": self._visible.copy(), + "_idx": self._idx.copy(), + } + i1, i2 = link.idx + if i1 in self._idx and i2 in self._idx: + self._affinity += link.affinity + self._links.append(link) + return False + if link.j1.label in self._visible and link.j2.label in self._visible: + return False + self.add_joint(link.j1) + self.add_joint(link.j2) + self._affinity += link.affinity + self._links.append(link) + return True + + def calc_pairwise_distances(self): + return pdist(self.xy, metric="sqeuclidean") + + +class Assembler: + def __init__( + self, + data, + *, + max_n_individuals, + n_multibodyparts, + graph=None, + paf_inds=None, + greedy=False, + pcutoff=0.1, + min_affinity=0.05, + min_n_links=2, + max_overlap=0.8, + identity_only=False, + nan_policy="little", + force_fusion=False, + add_discarded=False, + window_size=0, + method="m1", + ): + self.data = data + self.metadata = self.parse_metadata(self.data) + self.max_n_individuals = max_n_individuals + self.n_multibodyparts = n_multibodyparts + self.n_uniquebodyparts = self.n_keypoints - n_multibodyparts + self.greedy = greedy + self.pcutoff = pcutoff + self.min_affinity = min_affinity + self.min_n_links = min_n_links + 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.", stacklevel=2) + self.identity_only = identity_only & self._has_identity + self.nan_policy = nan_policy + self.force_fusion = force_fusion + self.add_discarded = add_discarded + self.window_size = window_size + self.method = method + self.graph = graph or self.metadata["paf_graph"] + self.paf_inds = paf_inds or self.metadata["paf"] + self._gamma = 0.01 + self._trees = dict() + self.safe_edge = False + self._kde = None + self.assemblies = dict() + self.unique = dict() + + def __getitem__(self, item): + return self.data[self.metadata["imnames"][item]] + + @classmethod + def empty( + cls, + max_n_individuals, + n_multibodyparts, + n_uniquebodyparts, + graph, + paf_inds, + greedy=False, + pcutoff=0.1, + min_affinity=0.05, + min_n_links=2, + max_overlap=0.8, + identity_only=False, + nan_policy="little", + force_fusion=False, + add_discarded=False, + window_size=0, + method="m1", + ): + # Dummy data + n_bodyparts = n_multibodyparts + n_uniquebodyparts + data = { + "metadata": { + "all_joints_names": ["" for _ in range(n_bodyparts)], + "PAFgraph": graph, + "PAFinds": paf_inds, + }, + "0": {}, + } + return cls( + data, + max_n_individuals=max_n_individuals, + n_multibodyparts=n_multibodyparts, + graph=graph, + paf_inds=paf_inds, + greedy=greedy, + pcutoff=pcutoff, + min_affinity=min_affinity, + min_n_links=min_n_links, + max_overlap=max_overlap, + identity_only=identity_only, + nan_policy=nan_policy, + force_fusion=force_fusion, + add_discarded=add_discarded, + window_size=window_size, + method=method, + ) + + @property + def n_keypoints(self): + return self.metadata["num_joints"] + + def calibrate(self, train_data_file): + df = pd.read_hdf(train_data_file) + try: + df.drop("single", level="individuals", axis=1, inplace=True) + except KeyError: + pass + n_bpts = len(df.columns.get_level_values("bodyparts").unique()) + if n_bpts == 1: + warnings.warn("There is only one keypoint; skipping calibration...", stacklevel=2) + return + + xy = df.to_numpy().reshape((-1, n_bpts, 2)) + frac_valid = np.mean(~np.isnan(xy), axis=(1, 2)) + # 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...", stacklevel=2) + return + + # TODO Normalize dists by longest length? + # TODO Smarter imputation technique (Bayesian? Grassmann averages?) + dists = np.vstack([pdist(data, "sqeuclidean") for data in xy]) + mu = np.nanmean(dists, axis=0) + missing = np.isnan(dists) + dists = np.where(missing, mu, dists) + try: + kde = gaussian_kde(dists.T) + kde.mean = mu + self._kde = kde + 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...", stacklevel=2) + + 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.") + + dists = assembly.calc_pairwise_distances() - self._kde.mean + mask = np.isnan(dists) + # Distance is undefined if the assembly is empty + if not len(assembly) or mask.all(): + if return_proba: + return np.inf, 0 + return np.inf + + if nan_policy == "little": + inds = np.flatnonzero(~mask) + dists = dists[inds] + inv_cov = self._kde.inv_cov[np.ix_(inds, inds)] + # Correct distance to account for missing observations + factor = self._kde.d / len(inds) + else: + # Alternatively, reduce contribution of missing values to the Mahalanobis + # distance to zero by substituting the corresponding means. + dists[mask] = 0 + mask.fill(False) + inv_cov = self._kde.inv_cov + factor = 1 + dot = dists @ inv_cov + mahal = factor * sqrt(np.sum((dot * dists), axis=-1)) + if return_proba: + proba = 1 - chi2.cdf(mahal, np.sum(~mask)) + return mahal, proba + return mahal + + def calc_link_probability(self, link): + if self._kde is None: + raise ValueError("Assembler should be calibrated first with training data.") + + i = link.j1.label + j = link.j2.label + ind = _conv_square_to_condensed_indices(i, j, self.n_multibodyparts) + mu = self._kde.mean[ind] + sigma = self._kde.covariance[ind, ind] + z = (link.length**2 - mu) / sigma + return 2 * (1 - 0.5 * (1 + erf(abs(z) / sqrt(2)))) + + @staticmethod + def _flatten_detections(data_dict): + ind = 0 + coordinates = data_dict["coordinates"][0] + confidence = data_dict["confidence"] + ids = data_dict.get("identity", None) + if ids is None: + 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, strict=False)): + if not np.any(coords): + continue + for xy, p, g in zip(coords, conf, id_, strict=False): + joint = Joint(tuple(xy), p.item(), i, ind, g) + ind += 1 + yield joint + + def extract_best_links(self, joints_dict, costs, trees=None): + links = [] + for ind in self.paf_inds: + s, t = self.graph[ind] + dets_s = joints_dict.get(s, None) + dets_t = joints_dict.get(t, None) + if dets_s is None or dets_t is None: + continue + if ind not in costs: + continue + lengths = costs[ind]["distance"] + if np.isinf(lengths).all(): + continue + aff = costs[ind][self.method].copy() + 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]) + dists = [] + for n, tree in enumerate(trees, start=1): + d, _ = tree.query(vecs) + dists.append(np.exp(-self._gamma * n * d)) + w = np.mean(dists, axis=0) + 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)) + candidates = sorted( + 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: + if i not in i_seen and j not in j_seen: + i_seen.add(i) + j_seen.add(j) + links.append(Link(dets_s[i], dets_t[j], w)) + 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] + 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, 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)) + return links + + def _fill_assembly(self, assembly, lookup, assembled, safe_edge, nan_policy): + stack = [] + visited = set() + tabu = [] + counter = itertools.count() + + def push_to_stack(i): + for j, link in lookup[i].items(): + if j in assembly._idx: + continue + if link.idx in visited: + continue + heapq.heappush(stack, (-link.affinity, next(counter), link)) + visited.add(link.idx) + + for idx in assembly._idx: + push_to_stack(idx) + + while stack and len(assembly) < self.n_multibodyparts: + _, _, best = heapq.heappop(stack) + i, j = best.idx + if i in assembly._idx: + new_ind = j + elif j in assembly._idx: + new_ind = i + else: + continue + if new_ind in assembled: + continue + if safe_edge: + 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() + continue + d = self.calc_assembly_mahalanobis_dist(assembly, nan_policy=nan_policy) + if d < d_old: + push_to_stack(new_ind) + try: + _, _, link = heapq.heappop(tabu) + heapq.heappush(stack, (-link.affinity, next(counter), link)) + except IndexError: + pass + else: + heapq.heappush(tabu, (d - d_old, next(counter), best)) + assembly.__dict__.update(assembly._dict) + assembly._dict = dict() + else: + assembly.add_link(best) + push_to_stack(new_ind) + + def build_assemblies(self, links): + lookup = defaultdict(dict) + for link in links: + i, j = link.idx + lookup[i][j] = link + lookup[j][i] = link + + assemblies = [] + assembled = set() + + # Fill the subsets with unambiguous, complete individuals + G = nx.Graph([link.idx for link in links]) + for chain in nx.connected_components(G): + if len(chain) == self.n_multibodyparts: + edges = [tuple(sorted(edge)) for edge in G.edges(chain)] + assembly = Assembly(self.n_multibodyparts) + for link in links: + i, j = link.idx + if (i, j) in edges: + success = assembly.add_link(link) + if success: + lookup[i].pop(j) + lookup[j].pop(i) + assembled.update(assembly._idx) + assemblies.append(assembly) + + if len(assemblies) == self.max_n_individuals: + return assemblies, assembled + + for link in sorted(links, key=lambda x: x.affinity, reverse=True): + if any(i in assembled for i in link.idx): + continue + assembly = Assembly(self.n_multibodyparts) + assembly.add_link(link) + 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) + lookup[j].pop(i) + assembled.update(assembly._idx) + assemblies.append(assembly) + + # Fuse superfluous assemblies + 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] + while len(assemblies) > self.max_n_individuals: + ds = [] + for i, j in itertools.combinations(range(len(assemblies)), 2): + if assemblies[j] not in assemblies[i]: + temp = assemblies[i] + assemblies[j] + d = self.calc_assembly_mahalanobis_dist(temp) + delta = d - max(ds_old[i], ds_old[j]) + ds.append((i, j, delta, d, temp)) + if not ds: + break + min_ = sorted(ds, key=lambda x: x[2]) + i, j, delta, d, new = min_[0] + if delta < 0 or len(min_) == 1: + assemblies[i] = new + assemblies.pop(j) + ds_old[i] = d + ds_old.pop(j) + else: + break + elif self.force_fusion: + assemblies = sorted(assemblies, key=len) + for nrow in range(n_extra): + assembly = assemblies[nrow] + candidates = [a for a in assemblies[nrow:] if assembly not in a] + if not candidates: + continue + if len(candidates) == 1: + candidate = candidates[0] + else: + dists = [] + for cand in candidates: + d = cdist(assembly.xy, cand.xy) + dists.append(np.nanmin(d)) + candidate = candidates[np.argmin(dists)] + ind = assemblies.index(candidate) + assemblies[ind] += assembly + else: + store = dict() + for assembly in assemblies: + if len(assembly) != self.n_multibodyparts: + for i in assembly._idx: + store[i] = assembly + used = [link for assembly in assemblies for link in assembly._links] + unconnected = [link for link in links if link not in used] + for link in unconnected: + i, j = link.idx + try: + if store[j] not in store[i]: + temp = store[i] + store[j] + store[i].__dict__.update(temp.__dict__) + assemblies.remove(store[j]) + for idx in store[j]._idx: + store[idx] = store[i] + except KeyError: + pass + + # Second pass without edge safety + for assembly in assemblies: + if len(assembly) != self.n_multibodyparts: + self._fill_assembly(assembly, lookup, assembled, False, "") + assembled.update(assembly._idx) + + return assemblies, assembled + + def _assemble(self, data_dict, ind_frame): + joints = list(self._flatten_detections(data_dict)) + if not joints: + return None, None + + bag = defaultdict(list) + for joint in joints: + bag[joint.label].append(joint) + + assembled = set() + + if self.n_uniquebodyparts: + unique = np.full((self.n_uniquebodyparts, 3), np.nan) + for n, ind in enumerate(range(self.n_multibodyparts, self.n_keypoints)): + dets = bag[ind] + if not dets: + continue + if len(dets) > 1: + det = max(dets, key=lambda x: x.confidence) + else: + det = dets[0] + # Mark the unique body parts as assembled anyway so + # they are not used later on to fill assemblies. + assembled.update(d.idx for d in dets) + if det.confidence <= self.pcutoff and not self.add_discarded: + continue + unique[n] = *det.pos, det.confidence + if np.isnan(unique).all(): + unique = None + else: + unique = None + + if not any(i in bag for i in range(self.n_multibodyparts)): + return None, unique + + if self.n_multibodyparts == 1: + assemblies = [] + for joint in bag[0]: + if joint.confidence >= self.pcutoff: + ass = Assembly(self.n_multibodyparts) + ass.add_joint(joint) + assemblies.append(ass) + return assemblies, unique + + if self.max_n_individuals == 1: + get_attr = operator.attrgetter("confidence") + ass = Assembly(self.n_multibodyparts) + for ind in range(self.n_multibodyparts): + joints = bag[ind] + if not joints: + continue + ass.add_joint(max(joints, key=get_attr)) + return [ass], unique + + if self.identity_only: + assemblies = [] + get_attr = operator.attrgetter("group") + temp = sorted( + (joint for joint in joints if np.isfinite(joint.confidence)), + key=get_attr, + ) + groups = itertools.groupby(temp, get_attr) + 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: + ass.add_joint(joint) + if len(ass): + assemblies.append(ass) + assembled.update(ass._idx) + else: + trees = [] + for j in range(1, self.window_size + 1): + tree = self._trees.get(ind_frame - j, None) + if tree is not None: + trees.append(tree) + + links = self.extract_best_links(bag, data_dict["costs"], trees) + if self._kde: + for link in links[::-1]: + p = max(self.calc_link_probability(link), 0.001) + link.affinity *= p + if link.affinity < self.min_affinity: + links.remove(link) + + if self.window_size >= 1 and links: + # Store selected edges for subsequent frames + vecs = np.vstack([link.to_vector() for link in links]) + self._trees[ind_frame] = cKDTree(vecs) + + assemblies, assembled_ = self.build_assemblies(links) + assembled.update(assembled_) + + # Remove invalid assemblies + 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: + discarded.update((link.j1, link.j2)) + 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] + else: + scores = [ass._affinity for ass in assemblies] + lst = list(zip(scores, assemblies, strict=False)) + assemblies = [] + while lst: + temp = max(lst, key=lambda x: x[0]) + lst.remove(temp) + assemblies.append(temp[1]) + for pair in lst[::-1]: + if temp[1].intersection_with(pair[1]) >= self.max_overlap: + lst.remove(pair) + if len(assemblies) > self.max_n_individuals: + assemblies = sorted(assemblies, key=len, reverse=True) + for assembly in assemblies[self.max_n_individuals :]: + for link in assembly._links: + discarded.update((link.j1, link.j2)) + assemblies = assemblies[: self.max_n_individuals] + + if self.add_discarded and discarded: + # Fill assemblies with unconnected body parts + for joint in sorted(discarded, key=lambda x: x.confidence, reverse=True): + if self.safe_edge: + for assembly in assemblies: + if joint.label in assembly._visible: + continue + d_old = self.calc_assembly_mahalanobis_dist(assembly) + assembly.add_joint(joint) + d = self.calc_assembly_mahalanobis_dist(assembly) + if d < d_old: + break + assembly.remove_joint(joint) + else: + dists = [] + for i, assembly in enumerate(assemblies): + if joint.label in assembly._visible: + continue + d = cdist(assembly.xy, np.atleast_2d(joint.pos)) + dists.append((i, np.nanmin(d))) + if not dists: + continue + min_ = sorted(dists, key=lambda x: x[1]) + ind, _ = min_[0] + assemblies[ind].add_joint(joint) + + return assemblies, unique + + def assemble(self, chunk_size=1, n_processes=None): + self.assemblies = dict() + self.unique = dict() + # Spawning (rather than forking) multiple processes does not + # 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: + self.assemblies[i] = assemblies + if unique is not None: + self.unique[i] = unique + else: + global wrapped # Hack to make the function pickable + + def wrapped(i): + return i, self._assemble(self[i], 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): + if assemblies: + self.assemblies[i] = assemblies + if unique is not None: + self.unique[i] = unique + pbar.update() + + def from_pickle(self, pickle_path): + with Path(pickle_path).open("rb") as file: + data = pickle.load(file) + self.unique = data.pop("single", {}) + self.assemblies = data + + @staticmethod + def parse_metadata(data): + params = dict() + 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["bpts"] = params["ibpts"] = range(params["num_joints"]) + params["imnames"] = [fn for fn in list(data) if fn != "metadata"] + return params + + def to_h5(self, output_name): + data = np.full( + ( + len(self.metadata["imnames"]), + self.max_n_individuals, + self.n_multibodyparts, + 4, + ), + fill_value=np.nan, + ) + for ind, assemblies in self.assemblies.items(): + for n, assembly in enumerate(assemblies): + data[ind, n] = assembly.data + index = pd.MultiIndex.from_product( + [ + ["scorer"], + map(str, range(self.max_n_individuals)), + map(str, range(self.n_multibodyparts)), + ["x", "y", "likelihood"], + ], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + temp = data[..., :3].reshape((data.shape[0], -1)) + df = pd.DataFrame(temp, columns=index) + df.to_hdf(output_name, key="ass") + + def to_pickle(self, output_name): + data = dict() + for ind, assemblies in self.assemblies.items(): + data[ind] = [ass.data for ass in assemblies] + if self.unique: + data["single"] = self.unique + with Path(output_name).open("wb") as file: + pickle.dump(data, file, pickle.HIGHEST_PROTOCOL) + + +@dataclass +class MatchedPrediction: + """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. + + Attributes: + prediction: A prediction made by a pose model. + score: The confidence score for the prediction. + ground_truth: If None, then this prediction is not matched to any ground truth + (this can happen when there are more predicted individuals than GT). + Otherwise, the ground truth assembly to which this prediction is matched. + oks: The OKS score between the prediction and the ground truth pose. + """ + + prediction: Assembly + score: float + ground_truth: Assembly | None + oks: float + + +def calc_object_keypoint_similarity( + xy_pred, + xy_true, + sigma, + margin=0, + symmetric_kpts=None, +): + visible_gt = ~np.isnan(xy_true).all(axis=1) + if visible_gt.sum() < 2: # At least 2 points needed to calculate scale + return np.nan + + true = xy_true[visible_gt] + scale_squared = np.prod(np.ptp(true, axis=0) + np.spacing(1) + margin * 2) + if np.isclose(scale_squared, 0): + return np.nan + + k_squared = (2 * sigma) ** 2 + denom = 2 * scale_squared * k_squared + if isinstance(sigma, np.ndarray): + denom = denom[visible_gt] + + if symmetric_kpts is None: + pred = xy_pred[visible_gt] + pred[np.isnan(pred)] = np.inf + dist_squared = np.sum((pred - true) ** 2, axis=1) + oks = np.exp(-dist_squared / denom) + return np.mean(oks) + else: + oks = [] + xy_preds = [xy_pred] + 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() + for pair in pairs: + tmp[pair, :] = tmp[pair[::-1], :] + xy_preds.append(tmp) + for xy_pred in xy_preds: + pred = xy_pred[visible_gt] + pred[np.isnan(pred)] = np.inf + dist_squared = np.sum((pred - true) ** 2, axis=1) + oks.append(np.mean(np.exp(-dist_squared / denom))) + return max(oks) + + +def match_assemblies( + predictions: list[Assembly], + ground_truth: list[Assembly], + sigma: float, + margin: int = 0, + symmetric_kpts: list[tuple[int, int]] | None = None, + greedy_matching: bool = False, + greedy_oks_threshold: float = 0.0, +) -> tuple[int, list[MatchedPrediction]]: + """Matches assemblies to ground truth predictions. + + Returns: + int: the total number of valid ground truth assemblies + list[MatchedPrediction]: a list containing all valid predictions, potentially + matched to ground truth assemblies. + """ + # Only consider assemblies of at least two keypoints + predictions = [a for a in predictions if len(a) > 1] + ground_truth = [a for a in ground_truth if len(a) > 1] + 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] + predictions = np.asarray(predictions)[inds_pred] + + # indices of unmatched ground truth assemblies + matched = [ + MatchedPrediction( + prediction=p, + score=(p.affinity if p.n_links else p.confidence), + ground_truth=None, + oks=0.0, + ) + for p in predictions + ] + + # Greedy assembly matching like in pycocotools + if greedy_matching: + matched_gt_indices = set() + for idx, pred in enumerate(predictions): + oks = [ + calc_object_keypoint_similarity( + pred.xy, + gt.xy, + sigma, + margin, + symmetric_kpts, + ) + for gt in ground_truth + ] + if np.all(np.isnan(oks)): + continue + + ind_best = np.nanargmax(oks) + + # if this gt already matched, and not a crowd, continue + if ind_best in matched_gt_indices: + continue + + # Only match the pred to the GT if the OKS value is above a given threshold + if oks[ind_best] < greedy_oks_threshold: + continue + + matched_gt_indices.add(ind_best) + matched[idx].ground_truth = ground_truth[ind_best] + matched[idx].oks = oks[ind_best] + + # Global rather than greedy assembly matching + else: + inds_true = list(range(len(ground_truth))) + mat = np.zeros((len(predictions), len(ground_truth))) + for i, a_pred in enumerate(predictions): + for j, a_true in enumerate(ground_truth): + oks = calc_object_keypoint_similarity( + a_pred.xy, + a_true.xy, + sigma, + margin, + symmetric_kpts, + ) + if ~np.isnan(oks): + mat[i, j] = oks + rows, cols = linear_sum_assignment(mat, maximize=True) + 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) + + return num_ground_truth, matched + + +def parse_ground_truth_data_file(h5_file): + df = pd.read_hdf(h5_file) + try: + df.drop("single", axis=1, level="individuals", inplace=True) + except KeyError: + pass + # Cast columns of dtype 'object' to float to avoid TypeError + # further down in _parse_ground_truth_data. + # TODO @deruyter92 2026-06-10 (#3362): pandas migration 3.0 requires us to add support for string dtype columns. + cols = df.select_dtypes(include="object").columns + if cols.to_list(): + df[cols] = df[cols].astype("float") + n_individuals = len(df.columns.get_level_values("individuals").unique()) + n_bodyparts = len(df.columns.get_level_values("bodyparts").unique()) + data = df.to_numpy().reshape((df.shape[0], n_individuals, n_bodyparts, -1)) + return _parse_ground_truth_data(data) + + +def _parse_ground_truth_data(data): + gt = dict() + for i, arr in enumerate(data): + temp = [] + for row in arr: + if np.isnan(row[:, :2]).all(): + continue + ass = Assembly.from_array(row) + temp.append(ass) + if not temp: + continue + gt[i] = temp + return gt + + +def find_outlier_assemblies(dict_of_assemblies, criterion="area", qs=(5, 95)): + if not hasattr(Assembly, criterion): + raise ValueError(f"Invalid criterion {criterion}.") + + if len(qs) != 2: + raise ValueError("Two percentiles (for lower and upper bounds) should be given.") + + tuples = [] + 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, strict=False) + vals = np.asarray(vals) + lo, up = np.percentile(vals, qs, interpolation="nearest") + inds = np.flatnonzero((vals < lo) | (vals > up)).tolist() + return list(set(frame_inds[i] for i in inds)) + + +def _compute_precision_and_recall( + num_gt_assemblies: int, + oks_values: np.ndarray, + oks_threshold: float, + recall_thresholds: np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + """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 + negatives + true positives). + oks_values: the OKS value to the matched GT assembly for each prediction + oks_threshold: the OKS threshold at which recall and precision are being + computed + recall_thresholds: the recall thresholds to use to compute scores + + Returns: + The precision and recall arrays at each recall threshold + """ + tp = np.cumsum(oks_values >= oks_threshold) + fp = np.cumsum(oks_values < oks_threshold) + rc = tp / num_gt_assemblies + pr = tp / (fp + tp + np.spacing(1)) + recall = rc[-1] + + # Guarantee precision decreases monotonically, see + # https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173 + for i in range(len(pr) - 1, 0, -1): + if pr[i] > pr[i - 1]: + pr[i - 1] = pr[i] + + inds_rc = np.searchsorted(rc, recall_thresholds, side="left") + precision = np.zeros(inds_rc.shape) + valid = inds_rc < len(pr) + precision[valid] = pr[inds_rc[valid]] + return precision, recall + + +def evaluate_assembly_greedy( + assemblies_gt: dict[Any, list[Assembly]], + assemblies_pred: dict[Any, list[Assembly]], + oks_sigma: float, + oks_thresholds: Iterable[float], + margin: int | float = 0, + symmetric_kpts: list[tuple[int, int]] | None = None, +) -> dict: + """Runs greedy mAP evaluation, as done by pycocotools. + + Args: + assemblies_gt: A dictionary mapping image ID (e.g. filepath) to ground truth + assemblies. Should contain all the same keys as ``assemblies_pred``. + assemblies_pred: A dictionary mapping image ID (e.g. filepath) to predicted + assemblies. Should contain all the same keys as ``assemblies_gt``. + oks_sigma: The sigma to use to compute OKS values for keypoints . + oks_thresholds: The OKS thresholds at which to compute precision & recall. + margin: The margin to use to compute bounding boxes from keypoints. + symmetric_kpts: The symmetric keypoints in the dataset. + """ + recall_thresholds = np.linspace( # np.linspace(0, 1, 101) + start=0.0, stop=1.00, num=int(np.round((1.00 - 0.0) / 0.01)) + 1, endpoint=True + ) + precisions = [] + recalls = [] + for oks_t in oks_thresholds: + all_matched = [] + total_gt_assemblies = 0 + for ind, gt_assembly in assemblies_gt.items(): + pred_assemblies = assemblies_pred.get(ind, []) + num_gt_assemblies, matched = match_assemblies( + pred_assemblies, + gt_assembly, + oks_sigma, + margin, + symmetric_kpts, + greedy_matching=True, + greedy_oks_threshold=oks_t, + ) + all_matched.extend(matched) + total_gt_assemblies += num_gt_assemblies + + if len(all_matched) == 0: + precisions.append(0.0) + recalls.append(0.0) + continue + + # Global sort of assemblies (across all images) by score + scores = np.asarray([-m.score for m in all_matched]) + sorted_pred_indices = np.argsort(scores, kind="mergesort") + 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) + precisions.append(p) + recalls.append(r) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "precisions": precisions, + "recalls": recalls, + "mAP": precisions.mean(), + "mAR": recalls.mean(), + } + + +def evaluate_assembly( + ass_pred_dict, + ass_true_dict, + oks_sigma=0.072, + 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, + ass_pred_dict, + oks_sigma=oks_sigma, + oks_thresholds=oks_thresholds, + margin=margin, + symmetric_kpts=symmetric_kpts, + ) + + # sigma is taken as the median of all COCO keypoint standard deviations + all_matched = [] + total_gt_assemblies = 0 + + gt_assemblies = ass_true_dict.items() + if with_tqdm: + gt_assemblies = tqdm(gt_assemblies) + + for ind, gt_assembly in gt_assemblies: + pred_assemblies = ass_pred_dict.get(ind, []) + num_gt, matched = match_assemblies( + pred_assemblies, + gt_assembly, + oks_sigma, + margin, + symmetric_kpts, + greedy_matching, + ) + all_matched.extend(matched) + total_gt_assemblies += num_gt + + if not all_matched: + return { + "precisions": np.array([]), + "recalls": np.array([]), + "mAP": 0.0, + "mAR": 0.0, + } + + conf_pred = np.asarray([match.score for match in all_matched]) + idx = np.argsort(-conf_pred, kind="mergesort") + # Sort matching score (OKS) in descending order of assembly affinity + oks = np.asarray([match.oks for match in all_matched])[idx] + recall_thresholds = np.linspace(0, 1, 101) + precisions = [] + recalls = [] + for t in oks_thresholds: + p, r = _compute_precision_and_recall(total_gt_assemblies, oks, t, recall_thresholds) + precisions.append(p) + recalls.append(r) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "precisions": precisions, + "recalls": recalls, + "mAP": precisions.mean(), + "mAR": recalls.mean(), + } diff --git a/deeplabcut/core/metrics/__init__.py b/deeplabcut/core/metrics/__init__.py new file mode 100644 index 0000000000..94397de57a --- /dev/null +++ b/deeplabcut/core/metrics/__init__.py @@ -0,0 +1,13 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from .api import compute_metrics, prepare_evaluation_data +from .bbox import compute_bbox_metrics +from .identity import compute_identity_scores diff --git a/deeplabcut/core/metrics/api.py b/deeplabcut/core/metrics/api.py new file mode 100644 index 0000000000..2792e61873 --- /dev/null +++ b/deeplabcut/core/metrics/api.py @@ -0,0 +1,183 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""API methods to get metrics for deep learning models.""" + +from __future__ import annotations + +import numpy as np + +import deeplabcut.core.metrics.distance_metrics as distance_metrics + + +def compute_metrics( + ground_truth: dict[str, np.ndarray], + predictions: dict[str, np.ndarray], + single_animal: bool = False, + unique_bodypart_gt: dict[str, np.ndarray] | None = None, + unique_bodypart_poses: dict[str, np.ndarray] | None = None, + pcutoff: float = -1, + oks_bbox_margin: int = 0, + oks_sigma: float | np.ndarray = 0.1, + per_keypoint_rmse: bool = False, + compute_detection_rmse: bool = True, +) -> dict: + """Computes pose estimation performance metrics. + + Given ground truth pose labels and predictions on a dataset, computes RMSE and pose + mAP/mAR using OKS. + + The image paths in the ground_truth dict must be the same as the ones in the + predictions dict. + + Single animal RMSE is computed by simply calculating the Euclidean distance between + each ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. OKS (or object keypoint similarity) is + a similarity metric for keypoints (you can read more about it and its definition + here: https://cocodataset.org/#keypoints-eval). RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + greater than a small threshold. Predictions that cannot be matched to any ground + truth with non-zero OKS are not used to compute RMSE. + + Args: + ground_truth: The ground truth pose for which to compute metrics in the dataset. + This should be a dictionary mapping strings (image UIDs, such as image + paths) to ground truth pose for the image. The pose arrays should be + in the format (num_individuals, num_bodyparts, 3), where the 3 values are + x, y and visibility. The ``num_individuals`` corresponds to the number of + individuals labeled in each image. + predictions: The predicted poses for which to compute metrics in the dataset. + This should be a dictionary mapping strings (image UIDs, such as image + paths) to pose predictions for the image. The pose arrays should be + in the format (num_predictions, num_bodyparts, 3), where the 3 values are + x, y and score. The number of predictions can be different to the number of + ground truth individuals labeled for an image. + single_animal: Whether the metrics are being computed on a single-animal or + multi-animal dataset. This has an impact on RMSE computation. + unique_bodypart_gt: If unique bodyparts are defined for the dataset, they should + be contained in this dict in the same format as the ``ground_truth`` dict. + unique_bodypart_poses: If unique bodyparts are defined for the dataset, the + predictions should be contained in this dict in the same format as the + ``predictions`` dict. + pcutoff: The threshold to compute the "rmse_cutoff" score (RMSE of all + predictions with score above the cutoff). + oks_bbox_margin: The margin to add around keypoints to compute the area for OKS + computation. + oks_sigma: The OKS sigma to use to compute pose. + per_keypoint_rmse: Compute per-keypoint RMSE values. + compute_detection_rmse: Computes detection RMSE (without animal assembly) if the + predictions are from a multi-animal model. + + Returns: + A dictionary containing keys "rmse", "rmse_cutoff", "mAP" and "mAR" mapping + to those metrics on the given dataset. + + If unique bodyparts are given, two extra keys "rmse_unique_bodyparts" and + "rmse_pcutoff_unique_bodyparts" are also returned, containing the metrics for + the unique bodyparts head. + + If `per_keypoint_evaluation=True`, "keypoint_rmse", "keypoint_rmse_cutoff" (and + optionally "unique_keypoint_rmse" and "unique_keypoint_rmse_cutoff") keys are + added, containing a list of floats representing the RMSE for each keypoint. + + Examples: + Define the p-cutoff, prediction, and target DataFrames: + + pcutoff = 0.5 + ground_truth = {"img0": np.array([[[1.0, 1.0, 2.0], ...], ...]), ...} + predictions = {"img0": np.array([[[2.0, 1.0, 0.4], ...], ...]), ...} + scores = compute_metrics(ground_truth, predictions, pcutoff=pcutoff) + print(scores) + + This yields the following output scores: + + { + "rmse": 1.0, + "rmse_pcutoff": 0.0, + "mAP": 84.2, + "mAR": 74.5 + } + """ + data = prepare_evaluation_data(ground_truth, predictions) + oks_scores = distance_metrics.compute_oks( + data=data, + oks_sigma=oks_sigma, + oks_bbox_margin=oks_bbox_margin, + ) + + data_unique = None + if unique_bodypart_gt is not None: + assert unique_bodypart_poses is not None + data_unique = prepare_evaluation_data(unique_bodypart_gt, unique_bodypart_poses) + + rmse_scores = distance_metrics.compute_rmse( + data, + single_animal, + pcutoff, + data_unique=data_unique, + per_keypoint_results=per_keypoint_rmse, + ) + results = dict(**rmse_scores, **oks_scores) + + if compute_detection_rmse and not single_animal: + det_rmse, det_rmse_p = distance_metrics.compute_detection_rmse( + data, + pcutoff, + data_unique=data_unique, + ) + results["rmse_detections"] = det_rmse + results["rmse_detections_pcutoff"] = det_rmse_p + + return results + + +def prepare_evaluation_data( + ground_truth: dict[str, np.ndarray], + predictions: dict[str, np.ndarray], +) -> list[tuple[np.ndarray, np.ndarray]]: + """Prepares predictions and ground truth pose to compute metrics. + + Only keeps ground truth and predicted assemblies with at least 2 valid keypoints. + Sets the coordinates for all keypoints that aren't visible (for ground truth, + visibility <= 0 and for predictions score <= 0) to ``np.nan``. + + Sorts valid predictions by score. + + Args: + ground_truth: For each image, the GT of shape (n_idv, n_bpt, 3). + predictions: For each image, the pose predictions of shape (n_pred, n_bpt, 3). + + Returns: + A list containing (ground truth pose, predicted pose) for each image in the + dataset, where the predicted pose is sorted from highest to lowest score. + """ + pose_data = [] + for image, gt in ground_truth.items(): + gt = gt.copy() + gt[gt[..., 2] <= 0] = np.nan + + # only keep ground truth pose with at least one keypoint + gt_mask = np.any(np.all(~np.isnan(gt), axis=-1), axis=-1) + gt = gt[gt_mask] + + pred = predictions[image][..., :3].copy() # PAF have 5 values; keep xy + score + pred[pred[..., 2] < 0] = np.nan + + # only keep predicted pose with at least two keypoints + pred_mask = np.any(np.all(~np.isnan(pred), axis=-1), axis=-1) + pred = pred[pred_mask] + + scores = np.nanmean(pred[:, :, 2], axis=-1) + pred_order = np.argsort(-scores, kind="mergesort") + pose_data.append((gt, pred[pred_order])) + + return pose_data diff --git a/deeplabcut/core/metrics/bbox.py b/deeplabcut/core/metrics/bbox.py new file mode 100644 index 0000000000..315162c131 --- /dev/null +++ b/deeplabcut/core/metrics/bbox.py @@ -0,0 +1,166 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Bounding box metrics. + +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 datetime import datetime +from unittest.mock import Mock, patch + +import numpy as np + +try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + with_pycocotools = True +except ModuleNotFoundError: + with_pycocotools = False + + +@patch("pycocotools.coco.print", Mock()) +@patch("pycocotools.cocoeval.print", Mock()) +def compute_bbox_metrics( + ground_truth: dict[str, dict], + detections: dict[str, dict], +) -> dict[str, float]: + """Computes bbox mAP and mAR metrics for bounding boxes. + + Args: + ground_truth: A dictionary mapping image UIDs (such as image paths or filenames) + to a ground truth labels dict. The labels dict should contain the keys + "width" (image width), "height" (image height) and "bboxes" (a numpy array + of shape (num_gt_bboxes, 4) containing the ground truth bounding boxes in + format xywh). + detections: A dictionary mapping image UIDs (such as image paths or filenames) + to a predicted bounding box dict. The detections dict should contain the + keys "bboxes" (a numpy array of shape (num_detected_bboxes, 4) containing + the predicted bounding boxes in format xywh) and "scores" (a numpy array of + length num_detected_bboxes containing the confidence score for each + predicted bounding box). + + Returns: + The bounding box mAP/mAR metrics in a dictionary. + + Raises: + ModuleNotFoundError: If ``pycocotools`` is not installed + ValueError: If there are mismatches in the keys of ground_truth and detections + """ + if not with_pycocotools: + raise ModuleNotFoundError("pycocotools not installed! can't compute bbox mAP") + + if len(detections) != len(ground_truth): + raise ValueError() + + coco = COCO() + 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"), + } + predictions = [] + for idx, (img, gt) in enumerate(ground_truth.items()): + img_id = idx + 1 + coco.dataset["images"].append( + { + "id": img_id, + "file_name": img, + "width": gt["width"], + "height": gt["height"], + } + ) + for bbox in gt["bboxes"][:, :4]: + ann_id = len(coco.dataset["annotations"]) + 1 + coco.dataset["annotations"].append( + { + "id": ann_id, + "image_id": img_id, + "category_id": 1, + "area": max(1, (bbox[2] * bbox[3]).item()), + "bbox": bbox, + "iscrowd": 0, + } + ) + + 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: + return { + "mAP@50:95": 0.0, + "mAP@50": 0.0, + "mAP@75": 0.0, + "mAR@50:95": 0.0, + "mAR@50": 0.0, + "mAR@75": 0.0, + } + + predictions = np.stack(predictions, axis=0) + coco.createIndex() + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="bbox") + coco_eval.evaluate() + coco_eval.accumulate() + return { + name: val + for name, val in [ + _get_metric(coco_eval, recall=False), + _get_metric(coco_eval, recall=False, iou_threshold=0.5), + _get_metric(coco_eval, recall=False, iou_threshold=0.75), + _get_metric(coco_eval, recall=True), + _get_metric(coco_eval, recall=True, iou_threshold=0.5), + _get_metric(coco_eval, recall=True, iou_threshold=0.75), + ] + } + + +def _get_metric( + coco_eval: COCOeval, + recall: bool = False, + iou_threshold: float | None = None, + area_rng: str = "all", + max_dets: int = 100, +) -> tuple[str, float]: + metric_name = "mAR" if recall else "mAP" + if iou_threshold is not None: + thresh = f"{int(100 * iou_threshold)}" + else: + low, high = coco_eval.params.iouThrs[0], coco_eval.params.iouThrs[-1] + thresh = f"{int(100 * low)}:{int(100 * high)}" + + aind = [i for i, aRng in enumerate(coco_eval.params.areaRngLbl) if aRng == area_rng] + mind = [i for i, mDet in enumerate(coco_eval.params.maxDets) if mDet == max_dets] + if recall: + s = coco_eval.eval["recall"] + if iou_threshold is not None: + t = np.where(iou_threshold == coco_eval.params.iouThrs)[0] + s = s[t] + s = s[:, :, aind, mind] + else: + s = coco_eval.eval["precision"] + if iou_threshold is not None: + t = np.where(iou_threshold == coco_eval.params.iouThrs)[0] + s = s[t] + s = s[:, :, :, aind, mind] + + if len(s[s > -1]) == 0: + mean_s = -1 + else: + mean_s = 100 * np.mean(s[s > -1]).item() + + return f"{metric_name}@{thresh}", mean_s diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py new file mode 100644 index 0000000000..78d3f00573 --- /dev/null +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -0,0 +1,463 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Implementations of methods to compute distance metrics such as RMSE or OKS.""" + +from __future__ import annotations + +import numpy as np + +import deeplabcut.core.metrics.matching as matching +from deeplabcut.core.crossvalutils import find_closest_neighbors +from deeplabcut.core.inferenceutils import calc_object_keypoint_similarity + + +def compute_oks_matrix( + ground_truth: np.ndarray, + predictions: np.ndarray, + 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. + + Args: + ground_truth: The GT poses for an image, shape (n_individuals, n_kpts, 2) + predictions: The predicted poses in the image, shape (n_pred, n_kpts, 2) + oks_sigma: The sigma value to use to compute OKS + oks_bbox_margin: The margin to add around keypoints when computing the area. + FIXME(niels) We should allow the use of ground truth bboxes to get area + + Returns: + A matrix of shape (n_pred, n_kpts) where entry (i, j) is the OKS between + prediction i and ground truth j. + """ + oks_matrix = np.zeros((len(predictions), len(ground_truth))) + for pred_idx, pred in enumerate(predictions): + for gt_idx, gt in enumerate(ground_truth): + oks_matrix[pred_idx, gt_idx] = calc_object_keypoint_similarity( + pred[:, :2], + gt[:, :2], + sigma=oks_sigma, + margin=oks_bbox_margin, + ) + + return oks_matrix + + +def compute_oks( + data: list[tuple[np.ndarray, np.ndarray]], + oks_bbox_margin: float = 0.0, + oks_sigma: float | np.ndarray = 0.1, + oks_thresholds: np.ndarray | None = None, + oks_recall_thresholds: np.ndarray | None = None, +) -> dict[str, float]: + """Computes the OKS for pose at different thresholds. + + Args: + data: The data for which to compute OKS mAP: a list containing (gt_poses, + predicted_poses) tuples, where gt_pose is an array of shape + (num_gt_individuals, num_bpts, 3) and predicted_poses is an array of shape + (num_predictions, num_bpts, 3). For the GT, the 3 coordinates are (x, y, + visibility) while for the pose they are (x, y, confidence score). + oks_sigma: The OKS sigma to use to compute pose. + oks_bbox_margin: The margin to add around keypoints to compute the area for OKS + computation. + oks_thresholds: The OKS thresholds at which to compute AP. If None, defaults to + (0.5, 0.55, 0.6, ..., 0.9, 0.95). + oks_recall_thresholds: The recall thresholds to use to compute mAP. If None, + defaults to the same default values used in pycocotools. + + Returns: + A dictionary containing mAP and mAR scores. + """ + if oks_thresholds is None: + oks_thresholds = np.linspace(0.5, 0.95, 10) + + if oks_recall_thresholds is None: + oks_recall_thresholds = np.linspace( + start=0.0, + stop=1.00, + num=int(np.round((1.00 - 0.0) / 0.01)) + 1, + endpoint=True, + ) + + total_gt = 0 + pose_data = [] + for gt, pred in data: + # filter data to only keep individuals with at least 2 valid keypoints + gt = gt[np.sum(np.all(~np.isnan(gt), axis=-1), axis=-1) > 1] + pred = pred[np.sum(np.all(~np.isnan(pred), axis=-1), axis=-1) > 1] + + oks_matrix = compute_oks_matrix( + gt[:, :, :2], + pred[:, :, :2], + oks_sigma=oks_sigma, + oks_bbox_margin=oks_bbox_margin, + ) + + total_gt += len(gt) + pose_data.append((gt, pred, oks_matrix)) + + precisions, recalls = [], [] + for oks_threshold in oks_thresholds: + matches = [] + for gt, pred, oks_matrix in pose_data: + image_matches = matching.match_greedy_oks( + gt, + pred, + oks_matrix=oks_matrix, + oks_threshold=oks_threshold, + ) + matches.extend(image_matches) + + if len(matches) == 0: # no predictions -> precision 0, recall 0 + return {"mAP": 0, "mAR": 0} + + scores = np.asarray([m.score for m in matches]) + match_order = np.argsort(-scores, kind="mergesort") + oks_values = np.asarray([m.oks for m in matches]) + oks_values = oks_values[match_order] + + tp = np.cumsum(oks_values >= oks_threshold) + fp = np.cumsum(oks_values < oks_threshold) + rc = tp / total_gt + pr = tp / (fp + tp + np.spacing(1)) + recall = rc[-1] + + # Guarantee precision decreases monotonically, see + # https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173 + for i in range(len(pr) - 1, 0, -1): + if pr[i] > pr[i - 1]: + pr[i - 1] = pr[i] + + inds_rc = np.searchsorted(rc, oks_recall_thresholds, side="left") + precision = np.zeros(inds_rc.shape) + valid = inds_rc < len(pr) + precision[valid] = pr[inds_rc[valid]] + + precisions.append(precision) + recalls.append(recall) + + precisions = np.asarray(precisions) + recalls = np.asarray(recalls) + return { + "mAP": 100 * precisions.mean().item(), + "mAR": 100 * recalls.mean().item(), + } + + +def match_predictions_for_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + single_animal: bool, + oks_bbox_margin: float = 0.0, +) -> list[matching.PotentialMatch]: + """Matches GT keypoints to predictions to compute RMSE. + + Single animal RMSE is computed by simply calculating the distance between each + ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + non-zero (greater than a small threshold). Predictions that cannot be matched to + any ground truth with non-zero OKS are not used to compute RMSE. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + single_animal: Whether this is a single animal dataset. + oks_bbox_margin: When single_animal is False, predictions are matched to GT + using OKS. This is the margin used to apply when computing the bbox from + the pose to compute OKS. + + Returns: + A list containing the predictions matched to ground truth. + + Raises: + ValueError: If `single_animal=True` but more than one ground truth/predicted + keypoint is found for an entry + """ + matches = [] + for gt, pred in data: + if single_animal: + if gt.shape[0] > 1 or pred.shape[0] > 1: + raise ValueError( + "At most 1 individual and 1 prediction can be given when computing " + f"single animal RMSE. Found gt={gt.shape}, pred={pred.shape}" + ) + + image_matches = [] + if gt.shape[0] == 1 and pred.shape[0] == 1: + match = matching.PotentialMatch.from_pose(pred[0]) + match.match(gt[0], oks=float("nan")) # OKS not needed for RMSE + image_matches.append(match) + else: + oks_matrix = compute_oks_matrix( + gt[:, :, :2], + pred[:, :, :2], + oks_sigma=0.1, + oks_bbox_margin=oks_bbox_margin, + ) + image_matches = matching.match_greedy_oks( + gt, + pred, + oks_matrix=oks_matrix, + oks_threshold=1e-6, + ) + + matches.extend(image_matches) + + return matches + + +def compute_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + single_animal: bool, + pcutoff: float | list[float], + data_unique: list[tuple[np.ndarray, np.ndarray]] | None = None, + per_keypoint_results: bool = False, + oks_bbox_margin: float = 0.0, +) -> dict[str, float]: + """Computes the RMSE for pose predictions. + + Single animal RMSE is computed by simply calculating the distance between each + ground truth keypoint and the corresponding prediction. + + Multi-animal RMSE is computed differently: predictions are first matched to ground + truth individuals using greedy OKS matching. RMSE is then computed only between + predictions and the ground truth pose they are matched to, only when the OKS is + non-zero (greater than a small threshold). Predictions that cannot be matched to + any ground truth with non-zero OKS are not used to compute RMSE. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + single_animal: Whether this is a single animal dataset. + pcutoff: The p-cutoff to use to compute RMSE. If a list, the cutoff for each + bodypart is set individually. The list must have length num_bodyparts + + num_unique_bodyparts. + data_unique: Unique bodypart ground truth and predictions to include in RMSE + computations, if there are any such bodyparts. + per_keypoint_results: Whether to compute the RMSE for each individual keypoint. + oks_bbox_margin: When single_animal is False, predictions are matched to GT + using OKS. This is the margin used to apply when computing the bbox from + the pose to compute OKS. + + Returns: + A dictionary matching metric names to values. It will at least have "rmse" and + "rmse_cutoff" keys. If `per_keypoint_results=True` and there is at least one + non-NaN pixel error it will also contain "rmse_keypoint_X" and + "rmse_cutoff_keypoint_X" keys for each bodypart, where X is the index of the + bodypart. + + Raises: + ValueError: If `single_animal=True` but more than one ground truth/predicted + keypoint is found for an entry + """ + matches = match_predictions_for_rmse(data, single_animal, oks_bbox_margin) + pixel_errors, keypoint_scores = None, None + if len(matches) > 0: + pixel_errors = np.stack([m.pixel_errors() for m in matches]) + keypoint_scores = np.stack([m.keypoint_scores() for m in matches]) + + error, support, cutoff_error, cutoff_support = 0, 0, 0, 0 + if pixel_errors is not None: + bpt_cutoffs = pcutoff + if not isinstance(pcutoff, (int, float)): + bpt_cutoffs = pcutoff[: pixel_errors.shape[1]] + + error, support, cutoff_error, cutoff_support = collect_pixel_errors( + pixel_errors, + keypoint_scores, + bpt_cutoffs, + ) + + unique_pixel_errors, unique_keypoint_scores = None, None + if data_unique is not None: + u_matches = match_predictions_for_rmse(data_unique, single_animal=True) + if len(u_matches) > 0: + unique_pixel_errors = np.stack([m.pixel_errors() for m in u_matches]) + unique_keypoint_scores = np.stack([m.keypoint_scores() for m in u_matches]) + + bpt_cutoffs = pcutoff + if not isinstance(pcutoff, (int, float)): + 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, + ) + error += u_error + support += u_support + cutoff_error += u_cutoff_error + cutoff_support += u_cutoff_support + + results = dict(rmse=float("nan"), rmse_pcutoff=float("nan")) + if support > 0: + results["rmse"] = float(error / support) + if cutoff_support > 0: + results["rmse_pcutoff"] = float(cutoff_error / cutoff_support) + + if per_keypoint_results: + bodypart_errors = [("rmse_keypoint", pixel_errors)] + if unique_pixel_errors is not None: + bodypart_errors.append(("rmse_unique_keypoint", unique_pixel_errors)) + + for key_prefix, bpt_errors in bodypart_errors: + for idx, keypoint_error in enumerate(bpt_errors.T): + rmse = float("nan") + if np.any(~np.isnan(keypoint_error)): + rmse = np.nanmean(keypoint_error).item() + results[f"{key_prefix}_{idx}"] = float(rmse) + + return results + + +def compute_detection_rmse( + data: list[tuple[np.ndarray, np.ndarray]], + pcutoff: float | list[float], + data_unique: list[tuple[np.ndarray, np.ndarray]] | None = None, +) -> tuple[float, float]: + """Computes the detection RMSE for pose predictions. + + The detection RMSE score does not take individual assemblies into account. It only + judges the performance of the detections, matching each predicted keypoint to the + closest ground truth for each bodypart. + + This is the same way multi-animal RMSE was computed in DeepLabCut 2.X. + + Args: + data: The data for which to compute RMSE. This is a list containing (gt_poses, + predicted_poses), where gt_pose is an array of shape (num_gt_individuals, + num_bpts, 3) and predicted_poses is an array of shape (num_predictions, + num_bpts, 3). For the GT, the 3 coordinates are (x, y, visibility) while for + the pose they are (x, y, confidence score). + pcutoff: The p-cutoff to use to compute RMSE. If a list, the cutoff for each + bodypart is set individually. The list must have length num_bodyparts + + num_unique_bodyparts. + data_unique: Unique bodypart ground truth and predictions to include in RMSE + computations, if there are any such bodyparts. + + Returns: + The detection RMSE and detection RMSE after removing all detections with a + score below the pcutoff. + """ + distances = [] + distances_cutoff = [] + for image_gt, image_pred in data: + 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=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)] + if len(bpt_gt) == 0 or len(bpt_pred) == 0: + continue + + if isinstance(pcutoff, (int, float)): + bpt_pcutoff = pcutoff + else: + bpt_pcutoff = pcutoff[bpt_index] + + # assignment of predicted bodyparts to ground truth + neighbors = find_closest_neighbors(bpt_gt, bpt_pred, k=3) + for gt_index, pred_index in enumerate(neighbors): + if pred_index != -1: + gt = bpt_gt[gt_index] + pred = bpt_pred[pred_index] + dist = np.linalg.norm(gt[:2] - pred[:2]) + distances.append(dist) + + score = bpt_pred[pred_index, 2] + if score >= bpt_pcutoff: + distances_cutoff.append(dist) + + 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}, {image_pred.shape}." + ) + + if len(image_gt) == 1 and len(image_pred) == 1: + unique_gt, unique_pred = image_gt[0], image_pred[0] + num_unique = unique_gt.shape[0] + unique_cutoffs = pcutoff + 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)): + dist = np.linalg.norm(gt[:2] - pred[:2]) + distances.append(dist) + + score = pred[2] + if isinstance(pcutoff, (int, float)): + bpt_pcutoff = unique_cutoffs + else: + bpt_pcutoff = unique_cutoffs[bpt_index] + + if score >= bpt_pcutoff: + distances_cutoff.append(dist) + + rmse, rmse_cutoff = float("nan"), float("nan") + if len(distances) == 0: + return rmse, rmse_cutoff + + distances = np.stack(distances) + if np.any(~np.isnan(distances)): + rmse = float(np.nanmean(distances).item()) + + if len(distances_cutoff) > 0: + distances_cutoff = np.stack(distances_cutoff) + if np.any(~np.isnan(distances_cutoff)): + rmse_cutoff = float(np.nanmean(distances_cutoff).item()) + + return rmse, rmse_cutoff + + +def collect_pixel_errors( + pixel_errors: np.ndarray, + keypoint_scores: np.ndarray, + pcutoff: float, +) -> tuple[float, int, float, int]: + """Collects pixel errors for RMSE computation. + + Args: + pixel_errors: The pixel errors to collect, of shape (num_matches, num_bodyparts) + keypoint_scores: The scores corresponding to the pixel errors, of shape + (num_matches, num_bodyparts). + pcutoff: The pcutoff to use when computing cutoff RMSE. + + Returns: error, support, cutoff_error, support_cutoff + error: The sum of all pixel errors. + support: The number of valid pixel errors. + cutoff_error: The sum of all pixel errors with score > pcutoff. + support_cutoff: The number of valid pixel errors with score > pcutoff. + """ + error = 0.0 + cutoff_error = 0.0 + support = np.sum(~np.isnan(pixel_errors)).item() + support_cutoff = 0 + if support > 0: + error += np.nansum(pixel_errors).item() + + cutoff_mask = keypoint_scores >= pcutoff + cutoff_pixel_errors = pixel_errors[cutoff_mask] + support_cutoff = np.sum(~np.isnan(cutoff_pixel_errors)).item() + if support_cutoff > 0: + cutoff_error = np.nansum(cutoff_pixel_errors).item() + + return error, support, cutoff_error, support_cutoff diff --git a/deeplabcut/core/metrics/identity.py b/deeplabcut/core/metrics/identity.py new file mode 100644 index 0000000000..684b797213 --- /dev/null +++ b/deeplabcut/core/metrics/identity.py @@ -0,0 +1,91 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Implementations of methods to compute identity prediction accuracy.""" + +from __future__ import annotations + +import numpy as np +from sklearn.metrics import accuracy_score + +from deeplabcut.core.crossvalutils import find_closest_neighbors + + +def compute_identity_scores( + individuals: list[str], + bodyparts: list[str], + predictions: dict[str, np.ndarray], + identity_scores: dict[str, np.ndarray], + ground_truth: dict[str, np.ndarray], +) -> dict[str, float]: + """ + FIXME: With DLCRNet all heatmap "peaks" above 0.01 were kept, with 1 keypoint and + 1 identity score map per peak. Then, for each ground truth keypoint, we selected + the prediction closest to it, and evaluated the identity score in that position. + This is no longer the case, as we're now evaluating after assembly. So we only + have num_individuals assemblies. + + Args: + individuals: + bodyparts: + predictions: (num_assemblies, num_bodyparts, 3) + identity_scores: (num_assemblies, num_bodyparts, num_individuals) + ground_truth: (num_individuals, num_bodyparts, 3) + + Returns: + + """ + if not len(predictions) == len(ground_truth): + raise ValueError("Mismatch between number of predictions and ground truth") + + all_bpts = np.asarray(len(individuals) * bodyparts) + ids = np.full((len(predictions), len(all_bpts), 2), np.nan) + for i, (image, pred) in enumerate(predictions.items()): + for j in range(len(individuals)): + for k in range(len(bodyparts)): + bpt_idx = len(bodyparts) * j + k + ids[i, bpt_idx, 0] = j + + # set keypoints that aren't visible to NaN + gt = ground_truth[image].copy() + gt[gt[..., 2] <= 0, :2] = np.nan + gt = gt[..., :2] + + id_scores = identity_scores[image] + + # reorder to (bodypart, individual, ...) + 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, 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)) + + # Remove NaN predictions from the bodypart predictions + indices_pred = np.all(np.isfinite(bpt_pred), axis=1) + bpt_pred = bpt_pred[indices_pred] + bpt_id_scores = bpt_id_scores[indices_pred] + + neighbors = find_closest_neighbors(bpt_gt[indices_gt], bpt_pred, k=3) + 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 = ids.reshape((len(predictions), len(individuals), len(bodyparts), 2)) + results = {} + for i, bpt in enumerate(bodyparts): + temp = ids[:, :, i].reshape((-1, 2)) + valid = np.isfinite(temp).all(axis=1) + y_true, y_pred = temp[valid].T + results[f"{bpt}_accuracy"] = accuracy_score(y_true, y_pred) + + return results diff --git a/deeplabcut/core/metrics/matching.py b/deeplabcut/core/metrics/matching.py new file mode 100644 index 0000000000..791bcae97f --- /dev/null +++ b/deeplabcut/core/metrics/matching.py @@ -0,0 +1,167 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Algorithms to match predictions to ground truth labels.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass +class PotentialMatch: + """A potential match between predicted pose and ground truth pose. + + Args: + pose: An array of shape (num_bodyparts, 3) + score: The score for the prediction. This could be the mean of the confidence + score for each bodypart, or another value representing how confident the + model is that this assembly is correct. + gt: None if no ground truth pose was matched to the prediction. If defined, the + ground truth to which the prediction is matched. It should be of shape + (num_bodyparts, 3), where the 3 values are x, y and visibility. + oks: The OKS score between the pose and the ground truth. + """ + + pose: np.ndarray + score: float + gt: np.ndarray | None = None + oks: float = 0.0 + + def keypoint_scores(self) -> np.ndarray: + """Returns: The confidence score for each bodypart in the predicted pose.""" + return self.pose[:, 2].copy() + + def pixel_errors(self) -> np.ndarray: + """ + Returns: + The distance (in pixels) between each predicted and ground truth bodypart. + If this prediction is unmatched, returns an array of length num_bodyparts + containing all NaNs. + """ + if self.gt is None: + return np.full(len(self.pose), np.nan) + + 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. + + Args: + gt: The ground truth to which the prediction is matched. The ground truth + pose should be of shape (num_bodyparts, 3), where the 3 values are x, y + and visibility. + oks: The OKS similarity between the ground truth and this. + """ + self.gt = gt + self.oks = oks + + @classmethod + 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(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()) + + +def match_greedy_oks( + ground_truth: np.ndarray, + predictions: np.ndarray, + oks_matrix: np.ndarray, + oks_threshold: float = 0.0, +) -> list[PotentialMatch]: + """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. + + Args: + ground_truth: The ground truth labels for an image, of shape (n_idv, n_bpt, 2) + predictions: The predictions for an image, of shape (n_idv, n_bpt, 2) + oks_matrix: A matrix of shape (n_pred, n_kpts) where entry (i, j) is the OKS + between prediction i and ground truth j. + oks_threshold: The min. OKS for a prediction to be matched to a GT pose + + Returns: + A list containing a PotentialMatch for each predicted pose in the given + predictions. + """ + matches = [PotentialMatch.from_pose(pose=pred) for pred in predictions] + matched_gt_indices = set() + for idx, _pred in enumerate(predictions): + oks = oks_matrix[idx] + if np.all(np.isnan(oks)): + continue + + ind_best = np.nanargmax(oks) + + # if this gt already matched, continue + if ind_best in matched_gt_indices: + continue + + # Only match the pred to the GT if the OKS value is above a given threshold + if oks[ind_best] < oks_threshold: + continue + + matched_gt_indices.add(ind_best) + matches[idx].match(gt=ground_truth[ind_best], oks=oks[ind_best]) + + return matches + + +def match_greedy_rmse( + ground_truth: np.ndarray, + predictions: np.ndarray, + keep_assemblies: bool = True, +) -> list[PotentialMatch]: + """Greedy matching of ground truth individuals to predicted individuals using RMSE. + + The predictions must be sorted by score before being passed to this function. + + Args: + ground_truth: The ground truth labels for an image, of shape (n_idv, n_bpt, 2) + predictions: The predictions for an image, of shape (n_idv, n_bpt, 2) + keep_assemblies: Whether to match predicted keypoints to ground truth keypoints + while enforcing that all bodyparts for a predicted individual are matched + to bodyparts from the same ground truth assembly. When set to False, this + corresponds to detection RMSE score. + + Returns: + A list containing a PotentialMatch for each predicted pose in the given + predictions. + """ + if not keep_assemblies: + raise NotImplementedError() + + matches = [PotentialMatch.from_pose(pose=pred) for pred in predictions] + matched_gt_indices = set() + for idx, pred in enumerate(predictions): + bpt_distances = np.linalg.norm(pred[:, :2] - ground_truth[:, :, :2], axis=-1) + if np.all(np.isnan(bpt_distances)): + continue + + distances = np.nanmean(bpt_distances, axis=-1) + ind_best = np.nanargmin(distances) + + # if this gt already matched, continue + if ind_best in matched_gt_indices: + continue + + matched_gt_indices.add(ind_best) + matches[idx].match( + gt=ground_truth[ind_best], + oks=float("nan"), # don't compute OKS here + ) + + return matches diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py new file mode 100644 index 0000000000..e0b094a788 --- /dev/null +++ b/deeplabcut/core/trackingutils.py @@ -0,0 +1,823 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +import abc +import math +import warnings +from collections import defaultdict + +import numpy as np +from filterpy.common import kinematic_kf +from filterpy.kalman import KalmanFilter +from matplotlib import patches +from numba import jit +from numba.core.errors import NumbaPerformanceWarning +from scipy.optimize import linear_sum_assignment +from scipy.stats import mode +from tqdm import tqdm + +warnings.simplefilter("ignore", category=NumbaPerformanceWarning) + +TRACK_METHODS = { + "box": "_bx", + "ctd": "_ctd", + "skeleton": "_sk", + "ellipse": "_el", + "transformer": "_tr", +} + + +def calc_iou(bbox1, bbox2): + x1 = max(bbox1[0], bbox2[0]) + y1 = max(bbox1[1], bbox2[1]) + x2 = min(bbox1[2], bbox2[2]) + y2 = min(bbox1[3], bbox2[3]) + 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) + + +class BaseTracker: + """Base class for a constant-velocity Kalman filter-based tracker.""" + + n_trackers = 0 + + def __init__(self, dim, dim_z): + self.kf = kinematic_kf( + dim, + 1, + dim_z=dim_z, + order_by_dim=False, + ) + self.id = self.__class__.n_trackers + self.__class__.n_trackers += 1 + self.time_since_update = 0 + self.age = 0 + self.hits = 0 + self.hit_streak = 0 + + def update(self, z): + self.time_since_update = 0 + self.hits += 1 + self.hit_streak += 1 + self.kf.update(z) + + def predict(self): + self.kf.predict() + self.age += 1 + if self.time_since_update > 0: + self.hit_streak = 0 + self.time_since_update += 1 + return self.state + + @property + def state(self): + return self.kf.x.squeeze()[: self.kf.dim_z] + + @state.setter + def state(self, state): + self.kf.x[: self.kf.dim_z] = state + + +class Ellipse: + def __init__(self, x, y, width, height, theta): + self.x = x + self.y = y + self.width = width + self.height = height + self.theta = theta # in radians + self._geometry = None + + @property + def parameters(self): + return self.x, self.y, self.width, self.height, self.theta + + @property + 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) + + if max_dist == 0: + max_dist = 1 + + cost1 = 1 - min(dist / max_dist, 1) + cost2 = abs(math.cos(self.theta - other_ellipse.theta)) + return 0.8 * cost1 + 0.2 * cost2 * cost1 + + def contains_points(self, xy, tol=0.1): + ca = math.cos(self.theta) + sa = math.sin(self.theta) + x_demean = xy[:, 0] - self.x + y_demean = xy[:, 1] - self.y + return ( + ((ca * x_demean + sa * y_demean) ** 2 / (0.5 * self.width) ** 2) + + ((sa * x_demean - ca * y_demean) ** 2 / (0.5 * self.height) ** 2) + ) <= 1 + tol + + def draw(self, show_axes=True, ax=None, **kwargs): + import matplotlib.pyplot as plt + from matplotlib.lines import Line2D + from matplotlib.transforms import Affine2D + + if ax is None: + ax = plt.subplot(111, aspect="equal") + el = patches.Ellipse( + xy=(self.x, self.y), + width=self.width, + height=self.height, + angle=np.rad2deg(self.theta), + **kwargs, + ) + ax.add_patch(el) + 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 + major.set_transform(trans) + minor.set_transform(trans) + ax.add_artist(major) + ax.add_artist(minor) + + +class EllipseFitter: + def __init__(self, sd=2): + self.sd = sd + self.x = None + self.y = None + self.params = None + self._coeffs = None + + def fit(self, xy): + self.x, self.y = xy[np.isfinite(xy).all(axis=1)].T + if len(self.x) < 3: + return None + if self.sd: + self.params = self._fit_error(self.x, self.y, self.sd) + else: + self._coeffs = self._fit(self.x, self.y) + self.params = self.calc_parameters(self._coeffs) + if not np.isnan(self.params).any(): + return Ellipse(*self.params) + return None + + @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. + + Args: + x (ndarray): 1D trajectory. + y (ndarray): 1D trajectory. + + Returns: + ndarray: 1D array 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))) + S1 = D1 @ D1.T + S2 = D1 @ D2.T + S3 = D2 @ D2.T + T = -np.linalg.inv(S3) @ S2.T + temp = S1 + S2 @ T + M = np.zeros_like(temp) + M[0] = temp[2] * 0.5 + M[1] = -temp[1] + M[2] = temp[0] * 0.5 + E, V = np.linalg.eig(M) + cond = 4 * V[0] * V[2] - V[1] ** 2 + a1 = V[:, cond > 0][:, 0] + a2 = T @ a1 + return np.hstack((a1, a2)) + + @staticmethod + @jit(nopython=True) + def _fit_error(x, y, sd): + """Fit a sd-sigma covariance error ellipse to the data. + + Args: + x (ndarray): 1D input of X coordinates. + y (ndarray): 1D input of Y coordinates. + sd (int): Size of the error ellipse in standard deviations. + + Returns: + list: Ellipse center, semi-axes length, and angle to the X-axis. + """ + cov = np.cov(x, y) + E, V = np.linalg.eigh(cov) # Returns the eigenvalues in ascending order + # r2 = chi2.ppf(2 * norm.cdf(sd) - 1, 2) + # height, width = np.sqrt(E * r2) + height, width = 2 * sd * np.sqrt(E) + a, b = V[:, 1] + rotation = math.atan2(b, a) % np.pi + return [np.mean(x), np.mean(y), width, height, rotation] + + @staticmethod + @jit(nopython=True) + def calc_parameters(coeffs): + """Calculate ellipse center coordinates, semi-axes lengths, and + the counterclockwise angle of rotation from the x-axis to the ellipse major axis. + Visit http://mathworld.wolfram.com/Ellipse.html + for how to estimate ellipse parameters. + + Args: + coeffs (list): Fitting coefficients. + + Returns: + list: Center (1D ndarray), semi-axes (1D ndarray), and angle (float). + """ + # The general quadratic curve has the form: + # ax^2 + 2bxy + cy^2 + 2dx + 2fy + g = 0 + a, b, c, d, f, g = coeffs + b *= 0.5 + d *= 0.5 + f *= 0.5 + + # Ellipse center coordinates + x0 = (c * d - b * f) / (b * b - a * c) + y0 = (a * f - b * d) / (b * b - a * c) + + # Semi-axes lengths + num = 2 * (a * f * f + c * d * d + g * b * b - 2 * b * d * f - a * c * g) + den1 = (b * b - a * c) * (np.sqrt((a - c) ** 2 + 4 * b * b) - (a + c)) + den2 = (b * b - a * c) * (-np.sqrt((a - c) ** 2 + 4 * b * b) - (a + c)) + major = np.sqrt(num / den1) + minor = np.sqrt(num / den2) + + # Angle to the horizontal + if b == 0: + if a < c: + phi = 0 + else: + phi = np.pi / 2 + else: + if a < c: + phi = np.arctan(2 * b / (a - c)) / 2 + else: + phi = np.pi / 2 + np.arctan(2 * b / (a - c)) / 2 + + return [x0, y0, 2 * major, 2 * minor, phi] + + +class EllipseTracker(BaseTracker): + def __init__(self, params): + super().__init__(dim=5, dim_z=5) + self.kf.R[2:, 2:] *= 10.0 + # High uncertainty to the unobservable initial velocities + self.kf.P[5:, 5:] *= 1000.0 + self.kf.P *= 10.0 + self.kf.Q[5:, 5:] *= 0.01 + self.state = params + + @BaseTracker.state.setter + def state(self, params): + state = np.asarray(params).reshape((-1, 1)) + super(EllipseTracker, type(self)).state.fset(self, state) + + +class SkeletonTracker(BaseTracker): + def __init__(self, n_bodyparts): + super().__init__(dim=n_bodyparts * 2, dim_z=n_bodyparts) + self.kf.Q[self.kf.dim_z :, self.kf.dim_z :] *= 10 + self.kf.R[self.kf.dim_z :, self.kf.dim_z :] *= 0.01 + self.kf.P[self.kf.dim_z :, self.kf.dim_z :] *= 1000 + + def update(self, pose): + flat = pose.reshape((-1, 1)) + empty = np.isnan(flat).squeeze() + if empty.any(): + H = self.kf.H.copy() + H[empty] = 0 + flat[empty] = 0 + self.kf.update(flat, H=H) + else: + super().update(flat) + + @BaseTracker.state.setter + def state(self, pose): + curr_pose = pose.copy() + empty = np.isnan(curr_pose).all(axis=1) + if empty.any(): + fill = np.nanmean(pose, axis=0) + curr_pose[empty] = fill + super(SkeletonTracker, type(self)).state.fset(self, curr_pose.reshape((-1, 1))) + + +class BoxTracker(BaseTracker): + def __init__(self, bbox): + super().__init__(dim=4, dim_z=4) + self.kf = KalmanFilter(dim_x=7, dim_z=4) + self.kf.F = np.array( + [ + [1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1], + ] + ) + self.kf.H = np.array( + [ + [1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + ] + ) + self.kf.R[2:, 2:] *= 10.0 + # Give high uncertainty to the unobservable initial velocities + self.kf.P[4:, 4:] *= 1000.0 + self.kf.P *= 10.0 + self.kf.Q[-1, -1] *= 0.01 + self.kf.Q[4:, 4:] *= 0.01 + self.state = bbox + + def update(self, bbox): + super().update(self.convert_bbox_to_z(bbox)) + + def predict(self): + if (self.kf.x[6] + self.kf.x[2]) <= 0: + self.kf.x[6] *= 0.0 + return super().predict() + + @property + def state(self): + return self.convert_x_to_bbox(self.kf.x)[0] + + @state.setter + def state(self, bbox): + state = self.convert_bbox_to_z(bbox) + super(BoxTracker, type(self)).state.fset(self, state) + + @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. + """ + 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)) + 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)) + + @staticmethod + def convert_bbox_to_z(bbox): + """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. + """ + w = bbox[2] - bbox[0] + h = bbox[3] - bbox[1] + x = bbox[0] + w / 2.0 + y = bbox[1] + h / 2.0 + s = w * h # scale is just area + r = w / float(h) + return np.array([x, y, s, r]).reshape((4, 1)) + + +class SORTBase(metaclass=abc.ABCMeta): + def __init__(self): + self.n_frames = 0 + self.trackers = [] + + @abc.abstractmethod + def track(self): + pass + + +class SORTEllipse(SORTBase): + def __init__(self, max_age, min_hits, iou_threshold, sd=2): + self.max_age = max_age + self.min_hits = min_hits + self.iou_threshold = iou_threshold + self.fitter = EllipseFitter(sd) + EllipseTracker.n_trackers = 0 + super().__init__() + + def track(self, poses, identities=None): + self.n_frames += 1 + + trackers = np.zeros((len(self.trackers), 6)) + for i in range(len(trackers)): + trackers[i, :5] = self.trackers[i].predict() + empty = np.isnan(trackers).any(axis=1) + trackers = trackers[~empty] + for ind in np.flatnonzero(empty)[::-1]: + self.trackers.pop(ind) + + ellipses = [] + pred_ids = [] + for i, pose in enumerate(poses): + el = self.fitter.fit(pose) + if el is not None: + ellipses.append(el) + if identities is not None: + pred_ids.append(mode(identities[i], keepdims=False)[0]) + if not len(trackers): + matches = np.empty((0, 2), dtype=int) + unmatched_detections = np.arange(len(ellipses)) + unmatched_trackers = np.empty((0, 6), dtype=int) + else: + ellipses_trackers = [Ellipse(*t[:5]) for t in trackers] + cost_matrix = np.zeros((len(ellipses), len(ellipses_trackers))) + for i, el in enumerate(ellipses): + for j, el_track in enumerate(ellipses_trackers): + cost = el.calc_similarity_with(el_track) + if identities is not None: + match = 2 if pred_ids[i] == self.trackers[j].id_ else 1 + 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] + matches = [] + for row, col in zip(row_indices, col_indices, strict=False): + val = cost_matrix[row, col] + # diff = val - cost_matrix + # diff[row, col] += val + # if ( + # val < self.iou_threshold + # or np.any(diff[row] <= 0.2) + # or np.any(diff[:, col] <= 0.2) + # ): + if val < self.iou_threshold: + unmatched_detections.append(row) + unmatched_trackers.append(col) + else: + matches.append([row, col]) + if not len(matches): + matches = np.empty((0, 2), dtype=int) + else: + matches = np.stack(matches) + unmatched_trackers = np.asarray(unmatched_trackers) + unmatched_detections = np.asarray(unmatched_detections) + + animalindex = [] + for t, tracker in enumerate(self.trackers): + if t not in unmatched_trackers: + ind = matches[matches[:, 1] == t, 0][0] + animalindex.append(ind) + tracker.update(ellipses[ind].parameters) + else: + animalindex.append(-1) + + for i in unmatched_detections: + trk = EllipseTracker(ellipses[i].parameters) + if identities is not None: + trk.id_ = mode(identities[i], keepdims=False)[0] + self.trackers.append(trk) + animalindex.append(i) + + i = len(self.trackers) + 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): + ret.append( + 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 + # remove dead tracklet + if trk.time_since_update > self.max_age: + self.trackers.pop(i) + + if len(ret) > 0: + return np.concatenate(ret) + return np.empty((0, 7)) + + +class SORTSkeleton(SORTBase): + def __init__(self, n_bodyparts, max_age=20, min_hits=3, oks_threshold=0.5): + self.n_bodyparts = n_bodyparts + self.max_age = max_age + self.min_hits = min_hits + self.oks_threshold = oks_threshold + SkeletonTracker.n_trackers = 0 + super().__init__() + + @staticmethod + def weighted_hausdorff(x, y): + # Modified from scipy source code: + # - to restrict its use to 2D + # - to get rid of shuffling (since arrays are only (nbodyparts * 3) element long) + # TODO - factor in keypoint confidence (and weight by # of observations??) + cmax = 0 + for i in range(x.shape[0]): + no_break_occurred = True + cmin = np.inf + for j in range(y.shape[0]): + d = (x[i, 0] - y[j, 0]) ** 2 + (x[i, 1] - y[j, 1]) ** 2 + if d < cmax: + no_break_occurred = False + break + if d < cmin: + cmin = d + if cmin != np.inf and cmin > cmax and no_break_occurred: + cmax = cmin + return np.sqrt(cmax) + + @staticmethod + def object_keypoint_similarity(x, y): + mask = ~np.isnan(x * y).all(axis=1) # Intersection visible keypoints + 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 + oks = np.exp(-0.5 * (dist / (0.05 * scale)) ** 2) + return np.mean(oks) + + def calc_pairwise_hausdorff_dist(self, poses, poses_ref): + mat = np.zeros((len(poses), len(poses_ref))) + for i, pose in enumerate(poses): + for j, pose_ref in enumerate(poses_ref): + mat[i, j] = self.weighted_hausdorff(pose, pose_ref) + return mat + + def calc_pairwise_oks(self, poses, poses_ref): + mat = np.zeros((len(poses), len(poses_ref))) + for i, pose in enumerate(poses): + for j, pose_ref in enumerate(poses_ref): + mat[i, j] = self.object_keypoint_similarity(pose, pose_ref) + return mat + + def track(self, poses): + self.n_frames += 1 + + if not len(self.trackers): + for pose in poses: + tracker = SkeletonTracker(self.n_bodyparts) + tracker.state = pose + self.trackers.append(tracker) + + poses_ref = [] + for _, tracker in enumerate(self.trackers): + pose_ref = tracker.predict() + poses_ref.append(pose_ref.reshape((-1, 2))) + + # mat = self.calc_pairwise_oks(poses, poses_ref) + mat = self.calc_pairwise_hausdorff_dist(poses, poses_ref) + 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] + # Remove matched detections with low OKS + # matches = [] + # for row, col in zip(row_indices, col_indices): + # if mat[row, col] < self.oks_threshold: + # unmatched_poses.append(row) + # unmatched_trackers.append(col) + # else: + # matches.append([row, col]) + # if not len(matches): + # matches = np.empty((0, 2), dtype=int) + # else: + # matches = np.stack(matches) + matches = np.c_[row_indices, col_indices] + + animalindex = [] + for t, tracker in enumerate(self.trackers): + if t not in unmatched_trackers: + ind = matches[matches[:, 1] == t, 0][0] + animalindex.append(ind) + tracker.update(poses[ind]) + else: + animalindex.append(-1) + + for i in unmatched_poses: + tracker = SkeletonTracker(self.n_bodyparts) + tracker.state = poses[i] + self.trackers.append(tracker) + animalindex.append(i) + + states = [] + i = len(self.trackers) + for tracker in reversed(self.trackers): + i -= 1 + if tracker.time_since_update > self.max_age: + self.trackers.pop(i) + continue + state = tracker.predict() + states.append(np.r_[state, [tracker.id, int(animalindex[i])]]) + if len(states) > 0: + return np.stack(states) + return np.empty((0, self.n_bodyparts * 2 + 2)) + + +class SORTBox(SORTBase): + def __init__(self, max_age, min_hits, iou_threshold): + self.max_age = max_age + self.min_hits = min_hits + self.iou_threshold = iou_threshold + BoxTracker.n_trackers = 0 + super().__init__() + + def track(self, dets): + self.n_frames += 1 + + trackers = np.zeros((len(self.trackers), 5)) + for i in range(len(trackers)): + trackers[i, :4] = self.trackers[i].predict() + empty = np.isnan(trackers).any(axis=1) + trackers = trackers[~empty] + 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) + + # update matched trackers with assigned detections + animalindex = [] + for t, trk in enumerate(self.trackers): + if t not in unmatched_trks: + d = matched[np.where(matched[:, 1] == t)[0], 0] + animalindex.append(d[0]) + trk.update(dets[d, :][0]) # update coordinates + else: + animalindex.append("nix") # lost trk! + + # create and initialise new trackers for unmatched detections + for i in unmatched_dets: + trk = BoxTracker(dets[i, :]) + self.trackers.append(trk) + animalindex.append(i) + + i = len(self.trackers) + 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): + ret.append( + 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 + # remove dead tracklet + if trk.time_since_update > self.max_age: + self.trackers.pop(i) + + if len(ret) > 0: + return np.concatenate(ret) + return np.empty((0, 5)) + + @staticmethod + def match_detections_to_trackers(detections, trackers, iou_threshold): + """Assigns detections to tracked object (both represented as bounding boxes) + + Returns 3 lists of matches, unmatched_detections and unmatched_trackers + """ + if not len(trackers): + return ( + np.empty((0, 2), dtype=int), + np.arange(len(detections)), + np.empty((0, 5), dtype=int), + ) + iou_matrix = np.zeros((len(detections), len(trackers)), dtype=np.float32) + + for d, det in enumerate(detections): + for t, trk in enumerate(trackers): + iou_matrix[d, t] = calc_iou(det, trk) + row_indices, col_indices = linear_sum_assignment(-iou_matrix) + + unmatched_detections = [] + for d, _ in enumerate(detections): + if d not in row_indices: + unmatched_detections.append(d) + unmatched_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, strict=False): + if iou_matrix[row, col] < iou_threshold: + unmatched_detections.append(row) + unmatched_trackers.append(col) + else: + matches.append([row, col]) + if not len(matches): + matches = np.empty((0, 2), dtype=int) + else: + matches = np.stack(matches) + return matches, np.array(unmatched_detections), np.array(unmatched_trackers) + + +def fill_tracklets(tracklets, trackers, animals, imname): + for content in trackers: + tracklet_id, pred_id = content[-2:].astype(int) + if tracklet_id not in tracklets: + tracklets[tracklet_id] = {} + if pred_id != -1: + tracklets[tracklet_id][imname] = np.asarray(animals[pred_id]) + else: # Resort to the tracker prediction + xy = np.asarray(content[:-2]) + pred = np.insert(xy, range(2, len(xy) + 1, 2), 1) + tracklets[tracklet_id][imname] = np.asarray(pred) + + +def calc_bboxes_from_keypoints(data, slack=0, offset=0): + data = np.asarray(data) + if data.shape[-1] < 3: + raise ValueError("Data should be of shape (n_animals, n_bodyparts, 3)") + + if data.ndim != 3: + data = np.expand_dims(data, axis=0) + bboxes = np.full((data.shape[0], 5), np.nan) + bboxes[:, :2] = np.nanmin(data[..., :2], axis=1) - slack # X1, Y1 + bboxes[:, 2:4] = np.nanmax(data[..., :2], axis=1) + slack # X2, Y2 + bboxes[:, -1] = np.nanmean(data[..., 2], axis=1) # Average confidence + bboxes[:, [0, 2]] += offset + return bboxes + + +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`. + + Args: + data (pandas.DataFrame): A multi-level DataFrame containing body part coordinates and likelihood values. + The index represents frames, and the columns follow a multi-level structure: + - Level 0: Scorer + - Level 1: Individuals + - Level 2: Body parts + - Level 3: Coordinates ("x" and "y") and "likelihood". + sd (float): The standard deviation used by the `EllipseFitter` for fitting ellipses. + + Returns: + numpy.ndarray: A 3D array of shape (A, F, 5), where: + - A is the number of individuals (excluding "single" if present). + - F is the number of frames. + - Each row contains ellipse parameters [cx, cy, width, height, angle]. + + Note: + - The method drops the "likelihood" column from the input DataFrame as it is not + relevant for ellipse fitting. + - If the "single" individual is present, it is excluded from the reconstruction process. + - The `EllipseFitter` is used to fit ellipses to the body part coordinates for each + individual in each frame. + - NaN values are assigned when no valid ellipse can be fitted. + """ + xy = data.droplevel("scorer", axis=1).drop("likelihood", axis=1, level=-1) + if "single" in xy: + xy.drop("single", axis=1, level="individuals", inplace=True) + animals = xy.columns.get_level_values("individuals").unique() + nrows = xy.shape[0] + ellipses = np.full((len(animals), nrows, 5), np.nan) + fitter = EllipseFitter(sd) + for n, animal in enumerate(animals): + data = xy.xs(animal, axis=1, level="individuals").values.reshape((nrows, -1, 2)) + for i, coords in enumerate(tqdm(data)): + el = fitter.fit(coords.astype(np.float64)) + if el is not None: + ellipses[n, i] = el.parameters + return ellipses + + +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.") + + if track_method == "ellipse": + tracker = SORTEllipse(max_age, min_hits, similarity_threshold) + elif track_method == "box": + tracker = SORTBox(max_age, min_hits, similarity_threshold) + else: + n_bodyparts = individuals[0][0].shape[0] + tracker = SORTSkeleton(n_bodyparts, max_age, min_hits, similarity_threshold) + + tracklets = defaultdict(dict) + all_hyps = dict() + for i, (multi, single) in enumerate(tqdm(individuals)): + if single is not None: + tracklets["single"][i] = single + if multi is None: + continue + if track_method == "box": + # TODO: get cropping parameters and utilize! + xy = calc_bboxes_from_keypoints(multi) + else: + xy = multi[..., :2] + hyps = tracker.track(xy) + all_hyps[i] = hyps + for hyp in hyps: + tracklet_id, pred_id = hyp[-2:].astype(int) + if pred_id != -1: + tracklets[tracklet_id][i] = multi[pred_id] + return tracklets, all_hyps diff --git a/deeplabcut/core/visualization.py b/deeplabcut/core/visualization.py new file mode 100644 index 0000000000..d9796f1c14 --- /dev/null +++ b/deeplabcut/core/visualization.py @@ -0,0 +1,234 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Visualization methods for.""" + +from __future__ import annotations + +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +def form_figure(nx, ny) -> tuple[plt.Figure, plt.Axes]: + """Forms a figure on which to plot images.""" + fig, ax = plt.subplots(frameon=False) + ax.set_xlim(0, nx) + ax.set_ylim(0, ny) + ax.axis("off") + ax.invert_yaxis() + fig.tight_layout() + return fig, ax + + +def visualize_scoremaps( + image: np.ndarray, + scmap: np.ndarray, +) -> tuple[plt.Figure, plt.Axes]: + """Plots scoremaps as an image overlay. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + + Returns: + The figure and axis on which the image scoremap was plot. + """ + ny, nx = np.shape(image)[:2] + fig, ax = form_figure(nx, ny) + ax.imshow(image) + ax.imshow(scmap, alpha=0.5) + return fig, ax + + +def visualize_locrefs( + image: np.ndarray, + scmap: np.ndarray, + locref_x: np.ndarray, + locref_y: np.ndarray, + step: int = 5, + zoom_width: int = 0, +) -> tuple[plt.Figure, plt.Axes]: + """Plots a scoremap and the corresponding location refinement field on an image. + + Args: + image: An image as a numpy array of shape (h, w, channels) + scmap: A scoremap of shape (h, w) + locref_x: The x-coordinate of the location refinement field, of shape (h, w) + locref_y: The y-coordinate of the location refinement field, of shape (h, w) + step: The step with which to plot the location refinement field. + zoom_width: The zoom width with which to plot the scoremaps. + + Returns: + The figure and axis on which the image scoremap and locref field were plot. + """ + fig, ax = visualize_scoremaps(image, scmap) + X, Y = np.meshgrid(np.arange(locref_x.shape[1]), np.arange(locref_x.shape[0])) + M = np.zeros(locref_x.shape, dtype=bool) + M[scmap < 0.5] = True + U = np.ma.masked_array(locref_x, mask=M) + V = np.ma.masked_array(locref_y, mask=M) + ax.quiver( + X[::step, ::step], + Y[::step, ::step], + U[::step, ::step], + V[::step, ::step], + color="r", + units="x", + scale_units="xy", + scale=1, + angles="xy", + ) + if zoom_width > 0: + maxloc = np.unravel_index(np.argmax(scmap), scmap.shape) + ax.set_xlim(maxloc[1] - zoom_width, maxloc[1] + zoom_width) + ax.set_ylim(maxloc[0] + zoom_width, maxloc[0] - zoom_width) + return fig, ax + + +def visualize_paf( + image: np.ndarray, + paf: np.ndarray, + step: int = 5, + colors: list | None = None, +) -> tuple[plt.Figure, plt.Axes]: + """Plots the PAF on top of the image. + + Args: + image: Shape (height, width, channels). The image on which the model was run. + paf: Shape (height, width, 2 * len(paf_graph)). The PAF output by the model. + step: The step with which to plot the scoremaps. + colors: The colormap to use. + + Returns: + The figure and axis on which the image PAF was plot. + """ + ny, nx = np.shape(image)[:2] + fig, ax = form_figure(nx, ny) + ax.imshow(image) + n_fields = paf.shape[2] + if colors is None: + colors = ["r"] * n_fields + for n in range(n_fields): + U = paf[:, :, n, 0] + V = paf[:, :, n, 1] + X, Y = np.meshgrid(np.arange(U.shape[1]), np.arange(U.shape[0])) + M = np.zeros(U.shape, dtype=bool) + M[U**2 + V**2 < 0.5 * 0.5**2] = True + U = np.ma.masked_array(U, mask=M) + V = np.ma.masked_array(V, mask=M) + ax.quiver( + X[::step, ::step], + Y[::step, ::step], + U[::step, ::step], + V[::step, ::step], + scale=50, + headaxislength=4, + alpha=1, + width=0.002, + color=colors[n], + angles="xy", + ) + return fig, ax + + +def generate_model_output_plots( + output_folder: Path, + image_name: str, + bodypart_names: list[str], + bodyparts_to_plot: list[str], + image: np.ndarray, + scmap: np.ndarray, + locref: np.ndarray | None = None, + paf: np.ndarray | None = None, + paf_graph: list[tuple[int, int]] | None = None, + paf_all_in_one: bool = True, + paf_colormap: str = "rainbow", + output_suffix: str = "", +) -> None: + """Generates model output plots (maps) for an image and saves them to disk. + + Args: + output_folder: The folder in which the plots should be saved. + image_name: The name of the image for which the plots were generated. + bodypart_names: The names of bodyparts the model outputs. + bodyparts_to_plot: The names of bodyparts that should be plot. + image: Shape (height, width, channels). The image on which the model was run. + scmap: Shape (height, width, num_bodyparts). The scoremaps output by the model. + locref: Shape (height, width, num_bodyparts, 2). Optionally, the location + refinement fields output by the model. + paf: Shape (height, width, 2 * len(paf_graph)). Optionally, the part-affinity + fields output by the model. + paf_graph: Must be set if paf is not None. The PAF graph used to assemble. + paf_all_in_one: Whether to plot all PAFs in a single image. + 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] + if len(to_plot) > 1: + map_ = scmap[:, :, to_plot].sum(axis=2) + elif len(to_plot) == 1 and len(bodypart_names) > 1: + map_ = scmap[:, :, to_plot[0]] + else: + map_ = scmap[..., 0] + + fig1, _ = visualize_scoremaps(image, map_) + fig1.savefig(output_folder / _filename("scmap")) + + if locref is not None: + if len(to_plot) > 1: + map_ = scmap[:, :, to_plot] + locref_x_ = locref[:, :, to_plot, 0] + locref_y_ = locref[:, :, to_plot, 1] + # only get the locref fields around their respective detections + locref_x_[map_ < 0.5] = 0 + locref_y_[map_ < 0.5] = 0 + # combine locrefs + map_ = map_.sum(axis=2) + locref_x_ = locref_x_.sum(axis=2) + locref_y_ = locref_y_.sum(axis=2) + elif len(to_plot) == 1 and len(bodypart_names) > 1: + locref_x_ = locref[:, :, to_plot[0], 0] + locref_y_ = locref[:, :, to_plot[0], 1] + else: + locref_x_ = locref[..., 0] + locref_y_ = locref[..., 1] + + fig2, _ = visualize_locrefs(image, map_, locref_x_, locref_y_) + fig2.savefig(output_folder / _filename("locref")) + + if paf is not None: + if paf_graph is None: + raise ValueError("When plotting the PAF, you must pass the ``paf_graph``") + + edge_list = [] + 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])]) + + if paf_all_in_one: + inds = [elem[0] for elem in edge_list] + n_inds = len(inds) + cmap = plt.cm.get_cmap(paf_colormap, n_inds) + colors = cmap(range(n_inds)) + fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors) + fig3.savefig(output_folder / _filename("paf")) + else: + for inds, names in edge_list: + fig3, _ = visualize_paf(image, paf[:, :, [inds]]) + fig3.savefig(output_folder / _filename(f"paf_{'_'.join(names)}")) + + plt.close("all") diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py new file mode 100644 index 0000000000..891d4510f4 --- /dev/null +++ b/deeplabcut/core/weight_init.py @@ -0,0 +1,182 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Classes to configure how to initialize model weights.""" + +from __future__ import annotations + +import warnings +from pathlib import Path + +import numpy as np +from pydantic import field_validator, model_validator +from typing_extensions import Self + +from deeplabcut.core.config import DLCBaseConfig +from deeplabcut.core.config.validation import NDArrayInt + + +class WeightInitialization(DLCBaseConfig): + """Configures weights initialization when transfer learning or fine-tuning models. + + Args: + snapshot_path: The path to the snapshot used to initialize pose model weights + when training a model. + detector_snapshot_path: The path to the snapshot used to initialize detector + weights when training a model. + dataset: Optionally, the dataset on which the snapshots were trained. Required + when fine-tuning SuperAnimal models. + with_decoder: Whether to load the decoder weights as well. + memory_replay: Only when ``with_decoder=True``. Whether to train the model with + memory replay, so that it predicts all SuperAnimal (or previous project) + bodyparts. + conversion_array: The mapping from SuperAnimal (or other project, on which the + weights were trained) to project bodyparts. Required when + `with_decoder=True`. + An array [7, 0, 1] means the project has 3 bodyparts, where the 1st bodypart + corresponds to the 8th bodypart in the pretrained model, the 2nd to the 1st + and the 3rd to the 2nd (as arrays are 0-indexed). + bodyparts: Optionally, the name of each bodypart entry in the conversion array. + """ + + snapshot_path: Path | None = None + detector_snapshot_path: Path | None = None + dataset: str | None = None + with_decoder: bool = False + memory_replay: bool = False + conversion_array: NDArrayInt | None = None + bodyparts: list[str] | None = None + + @model_validator(mode="after") + def _validate_options(self) -> Self: + if self.memory_replay and not self.with_decoder: + raise ValueError( + "You cannot train a model with memory replay if you do not keep the " + "decoder layers (``with_decoder=True``), but you passed " + "`memory_replay=True` and `with_decoder=False`. Please change your " + "WeightInitialization parameters." + ) + + if self.with_decoder and self.conversion_array is None: + raise ValueError( + "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( + "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 len(self.conversion_array) != len(self.bodyparts): + raise ValueError( + f"There must be the same number of elements in the bodyparts list " + f"and conv. array; found {self.bodyparts}, {self.conversion_array}" + ) + return self + + @field_validator("snapshot_path", "detector_snapshot_path", mode="before") + @classmethod + def _coerce_null_path(cls, v): + if v is None or v == "None": + return None + return v + + @classmethod + def from_dict(cls, data: dict) -> Self: + if "snapshot_path" not in data: + return cls.from_dict_legacy(data) + return cls.model_validate(data) + + @classmethod + def from_dict_legacy(cls, data: dict) -> Self: + """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") + if conversion_array is not None: + conversion_array = np.array(conversion_array, dtype=int) + + return cls( + snapshot_path=utils.get_super_animal_snapshot_path( + dataset=data["dataset"], + model_name="hrnet_w32", + ), + detector_snapshot_path=utils.get_super_animal_snapshot_path( + dataset=data["dataset"], + model_name="fasterrcnn_resnet50_fpn_v2", + ), + with_decoder=data["with_decoder"], + memory_replay=data["memory_replay"], + conversion_array=conversion_array, + bodyparts=data.get("bodyparts"), + ) + + @staticmethod + def build( + cfg: dict, + super_animal: str, + model_name: str = "hrnet_w32", + detector_name: str = "fasterrcnn_resnet50_fpn_v2", + with_decoder: bool = False, + memory_replay: bool = False, + customized_pose_checkpoint: str | None = None, + customized_detector_checkpoint: str | None = None, + ) -> WeightInitialization: + """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` + instead. + + Args: + cfg: The project's configuration. + super_animal: The SuperAnimal model with which to initialize weights. + model_name: The name of the model architecture for which to load the weights + (defaults to "hrnet_w32" for backwards compatibility). + detector_name: The name of the detector architecture for which to load the + weights (defaults to "fasterrcnn_resnet50_fpn_v2" for backwards + compatibility). + with_decoder: Whether to load the decoder weights as well. If this is true, + a conversion table must be specified for the given SuperAnimal in the + project configuration file. See + ``deeplabcut.modelzoo.utils.create_conversion_table`` to create a + conversion table. + memory_replay: Only when ``with_decoder=True``. Whether to train the model + with memory replay, so that it predicts all SuperAnimal bodyparts. + customized_pose_checkpoint: A customized SuperAnimal pose checkpoint, as an + alternative to the Hugging Face one + customized_detector_checkpoint: A customized SuperAnimal detector + checkpoint, as an alternative to the Hugging Face one + + Returns: + 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 " + "`deeplabcut.modelzoo` instead." + ) + warnings.warn(deprecation_warning, DeprecationWarning, stacklevel=2) + + return build_weight_init( + cfg, + super_animal, + model_name, + detector_name, + with_decoder, + memory_replay, + customized_pose_checkpoint, + customized_detector_checkpoint, + ) diff --git a/deeplabcut/create_project/__init__.py b/deeplabcut/create_project/__init__.py index 5e83e4fc20..d87e92227d 100644 --- a/deeplabcut/create_project/__init__.py +++ b/deeplabcut/create_project/__init__.py @@ -1,3 +1,13 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# from deeplabcut.create_project.add import add_new_videos from deeplabcut.create_project.demo_data import load_demo_data from deeplabcut.create_project.modelzoo import ( diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index bcd9d198ca..6ca399576b 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -1,108 +1,141 @@ -""" -DeepLabCut2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" - - -def add_new_videos(config, videos, copy_videos=False, coords=None): +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +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 + + +def add_new_videos( + config: str | Path, + videos: list[str | Path], + copy_videos=False, + coords=None, + extract_frames=False, +): + """Add new videos to the config file at any stage of the project. + + Args: + config (string): String containing the full path of the config file in the project. + videos (list): A list of strings containing the full paths of the videos to include in the project. + copy_videos (bool, optional): If True, the videos will be copied to your + project/videos directory. If False, symlinks of the videos are copied + instead. Defaults to False. + coords (list, optional): A list containing the list of cropping coordinates of the video. Defaults to None. + extract_frames (bool, optional): If True, extract_frames will be run on the new + videos. Defaults to False. + + 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"], + ) + + 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]], + ) + + 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]], + ) """ - Add new videos to the config file at any stage of the project. - - Parameters - ---------- - config : string - String containing the full path of the config file in the project. - - videos : list - A list of string containing the full paths of the videos to include in the project. - - copy_videos : bool, optional - If this is set to True, the symlink of the videos are copied to the project/videos directory. The default is - ``False``; if provided it must be either ``True`` or ``False``. - coords: list, optional - A list containing the list of cropping coordinates of the video. The default is set to None. - Examples - -------- - Video will be added, with cropping dimenions according to the frame dimensinos of 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 dimenions [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 dimenions [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 - from pathlib import Path - - from deeplabcut.utils import auxiliaryfunctions - from deeplabcut.utils.auxfun_videos import VideoReader + config = Path(config).absolute() # Read the config file cfg = auxiliaryfunctions.read_config(config) - video_path = Path(config).parents[0] / "videos" - data_path = Path(config).parents[0] / "labeled-data" - videos = [Path(vp) for vp in videos] + # deal with user passing a single video to add + if isinstance(videos, str): + videos = [videos] - dirs = [data_path / Path(i.stem) for i in videos] + video_path = config.parent / "videos" + data_path = config.parent / "labeled-data" + videos = [Path(vp).absolute() for vp in videos] + + dirs = [data_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] if copy_videos: - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists(): pass else: print("Copying the videos") shutil.copy(os.fspath(src), os.fspath(dst)) + else: - for src, dst in zip(videos, destinations): + # 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, strict=False): if dst.exists(): - pass - else: - print("Creating the symbolic link of the video") - src = str(src) - dst = str(dst) - os.symlink(src, dst) + print(f"Video {dst} already exists. Skipping...") + continue + try: + dst.symlink_to(src) + print(f"Created the symlink of {src} to {dst}") + except OSError: + try: + import subprocess + + subprocess.check_call(f"mklink {os.fspath(dst)} {os.fspath(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(f"{src} copied to {dst}") + videos = destinations if copy_videos: - videos = ( - destinations - ) # in this case the *new* location should be added to the config file + videos = destinations # in this case the *new* location should be added to the config file # adds the video list to the config.yaml file for idx, video in enumerate(videos): - try: - # 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: - video_path = os.readlink(video) - - vid = VideoReader(video_path) + video_key = Path(video).absolute() + vid = VideoReader(os.fspath(video_key)) if coords is not None: c = coords[idx] else: c = vid.get_bbox() - params = {video_path: {"crop": ", ".join(map(str, c))}} + params = {os.fspath(video_key): {"crop": ", ".join(map(str, c))}} if "video_sets_original" not in cfg: cfg["video_sets"].update(params) else: cfg["video_sets_original"].update(params) - auxiliaryfunctions.write_config(config, cfg) - print( - "New video was added to the project! Use the function 'extract_frames' to select frames for labeling." - ) + if extract_frames: + frame_extraction.extract_frames( + config, + userfeedback=False, + videos_list=[os.fspath(video) for video in videos], + ) + 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.") diff --git a/deeplabcut/create_project/demo_data.py b/deeplabcut/create_project/demo_data.py index 32e5c31a8d..22f428d948 100644 --- a/deeplabcut/create_project/demo_data.py +++ b/deeplabcut/create_project/demo_data.py @@ -1,63 +1,67 @@ -""" -DeepLabCut2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 pathlib import Path import deeplabcut +from deeplabcut.core.engine import Engine from deeplabcut.utils import auxiliaryfunctions -def load_demo_data(config, createtrainingset=True): - """ - Loads the demo data. Make sure that you are in the same directory where you have downloaded or cloned the deeplabcut. +def load_demo_data( + config: str | Path, + 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. - Parameter - ---------- - config : string - Full path of the config.yaml file of the provided demo dataset as a string. + Args: + config (str | Path): Full path of the config.yaml file of the provided demo + dataset. + createtrainingset (bool): Boolean variable indicating if a training set shall be + created. + engine (Engine): The Engine to create the training set for if a training set + shall be created. - Example - -------- - >>> deeplabcut.load_demo_data('config.yaml') - -------- - """ - config = Path(config).resolve() - config = str(config) + Examples: + deeplabcut.load_demo_data("config.yaml") + """ + config = Path(config).absolute() transform_data(config) if createtrainingset: print("Loaded, now creating training data...") - deeplabcut.create_training_dataset(config, num_shuffles=1) + deeplabcut.create_training_dataset(config, num_shuffles=1, engine=engine) -def transform_data(config): - """ - This function adds the full path to labeling dataset. +def transform_data(config: Path) -> None: + """This function adds the full path to labeling dataset. + It also adds the correct path to the video file in the config file. """ - + config = Path(config).absolute() + project_path = config.parent cfg = auxiliaryfunctions.read_config(config) - project_path = str(Path(config).parents[0]) - cfg["project_path"] = project_path - if "Reaching" in project_path: - video_file = os.path.join(project_path, "videos", "reachingvideo1.avi") - elif "openfield" in project_path: - video_file = os.path.join(project_path, "videos", "m4s1.mp4") + if "Reaching" in project_path.parts: + video_file = project_path / "videos" / "reachingvideo1.avi" + elif "openfield" in project_path.parts: + video_file = project_path / "videos" / "m4s1.mp4" else: - print("This is not an offical demo dataset.") + print("This is not an official demo dataset.") + return 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"][os.fspath(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 7a82ca50ad..5db6a3e278 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -1,35 +1,52 @@ -""" -DeepLabCut 2.1.8 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut - -Please see AUTHORS for contributors. -https://github.com/DeepLabCu/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 collections.abc import Sequence from pathlib import Path import yaml +from dlclibrary import get_available_detectors +from dlclibrary.dlcmodelzoo.modelzoo_download import ( + MODELOPTIONS, + download_huggingface_model, + get_available_datasets, + get_available_models, +) import deeplabcut -from deeplabcut.utils import auxiliaryfunctions, auxfun_models - -Modeloptions = [ - "full_human", - "full_cat", - "full_dog", - "primate_face", - "mouse_pupil_vclose", - "horse_sideview", - "full_macaque", - "full_cheetah", -] # just expand this list with new projects +from deeplabcut.core.config import ProjectConfig, write_config +from deeplabcut.core.deprecation import renamed_parameter +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset.metadata import ( + DataSplit, + ShuffleMetadata, + TrainingDatasetMetadata, +) +from deeplabcut.generate_training_dataset.trainingsetmanipulation import ( + MakeInference_yaml, +) +from deeplabcut.modelzoo.utils import get_super_animal_project_cfg +from deeplabcut.pose_estimation_pytorch.config import ( + PoseMetadata, + make_pytorch_test_config, +) +from deeplabcut.pose_estimation_pytorch.modelzoo.utils import load_super_animal_config +from deeplabcut.utils import auxiliaryfunctions + +Modeloptions = MODELOPTIONS # backwards compatibility for COLAB NOTEBOOK def MakeTrain_pose_yaml(itemstochange, saveasconfigfile, defaultconfigfile): - raw = open(defaultconfigfile).read() + raw = Path(defaultconfigfile).open().read() docs = [] for raw_doc in raw.split("\n---"): try: @@ -40,8 +57,7 @@ def MakeTrain_pose_yaml(itemstochange, saveasconfigfile, defaultconfigfile): for key in itemstochange.keys(): docs[0][key] = itemstochange[key] docs[0]["max_input_size"] = 1500 - with open(saveasconfigfile, "w") as f: - yaml.dump(docs[0], f) + write_config(saveasconfigfile, docs[0]) return docs[0] @@ -62,18 +78,18 @@ def MakeTest_pose_yaml(dictionary, keys2save, saveasfile): # yaml.dump(dict_test, f) +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_pretrained_human_project( project, experimenter, videos, working_directory=None, copy_videos=False, - videotype=".mp4", + video_extensions: str | Sequence[str] | None = None, 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', ..) @@ -84,7 +100,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, @@ -93,26 +111,161 @@ def create_pretrained_human_project( model="full_human", working_directory=working_directory, copy_videos=copy_videos, - videotype=videotype, + video_extensions=video_extensions, createlabeledvideo=createlabeledvideo, analyzevideo=analyzevideo, + engine=Engine.TF, ) +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_pretrained_project( - project, - experimenter, - videos, - model="full_human", - working_directory=None, - copy_videos=False, - videotype=None, - analyzevideo=True, - filtered=True, - createlabeledvideo=True, - trainFraction=None, + project: str, + experimenter: str, + videos: list[str], + model: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + analyzevideo: bool = True, + filtered: bool = True, + createlabeledvideo: bool = True, + trainFraction: float | None = None, + engine: Engine = Engine.PYTORCH, + multi_animal: bool = False, + individuals: list[str] | None = None, + net_name: str | None = None, + detector_name: str | None = None, ): + 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! + + http://modelzoo.deeplabcut.org + + Args: + project (string): String containing the name of the project. + experimenter (string): String containing the name of the experimenter. + model (string | None, optional): The model / dataset to use as basis for the + project. If None, the default model / dataset for the selected engine will + be used. Defaults to None. + videos (list[string]): A list of string containing the full paths of the videos + to include in the project. + working_directory (string, optional): The directory where the project will be + created. If None - the current working directory will be used. Defaults to + None. + copy_videos (bool, optional): If this is set to True, the videos are copied to + the ``videos`` directory. If it is False, symlink of the videos are copied + to the project/videos directory. + Note: on Windows, True is necessary when not running in Administrator mode. + The same applies whenever symlinks are disabled or unsupported. + Defaults to False. + analyzevideo (bool, optional): If true, then the video is analyzed and a labeled + video is created. If false, then only the project will be created and the + weights downloaded. + filtered (bool, optional): Indicates if filtered pose data output should be + plotted rather than frame-by-frame predictions. Filtered version can be + calculated with deeplabcut.filterpredictions(). Defaults to True. + createlabeledvideo (bool, optional): Specifies if a labeled video needs to be + created. Defaults to True. + trainFraction (float | None, optional): Fraction that will be used in + dlc-model/trainingset folder name. If None - default value (0.95) from new + projects will be used. Defaults to None. + engine (Engine, optional): Engine on which the pretrained weights are based. + Defaults to Engine.PYTORCH. + multi_animal (bool, optional): Specifies if the project is single or + multi-animal. Implemented only for Pytorch-based models. Defaults to False. + individuals (list[str] | None, optional): Only if multianimal is True. Defines + the names of the individuals. Defaults to None. + net_name (str | None, optional): Valid only if using Pytorch engine. Name of the + pose model on which the superanimal dataset has been trained on. If None - + "hrnet_w32" will be used as default. Defaults to None. + detector_name (str | None, optional): Valid only if using Pytorch engine. Name + of the detector model on which the superanimal dataset has been trained on. + If None - "fasterrcnn_resnet50_fpn_v2" will be used as default. Defaults to + None. + + Examples: + Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi: + + 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 + ) + + Windows: + + deeplabcut.create_pretrained_project( + "humanstrokestudy", + "Bill", + [r"C:\yourusername\rig-95\Videos\reachingvideo1.avi"], + r"C:\yourusername\analysis\project", + copy_videos=True, + ) + + On Windows, paths should be formatted as ``r`"C:\"`` or ``"C:\\"`` (i.e. a double backslash). """ + if engine == Engine.TF: + return create_pretrained_project_tensorflow( + project=project, + experimenter=experimenter, + videos=videos, + model=model, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + analyzevideo=analyzevideo, + filtered=filtered, + createlabeledvideo=createlabeledvideo, + trainFraction=trainFraction, + ) + elif engine == Engine.PYTORCH: + return create_pretrained_project_pytorch( + project=project, + experimenter=experimenter, + videos=videos, + dataset=model, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + analyze_video=analyzevideo, + filtered=filtered, + create_labeled_video=createlabeledvideo, + train_fraction=trainFraction, + multi_animal=multi_animal, + individuals=individuals, + net_name=net_name, + detector_name=detector_name, + ) + + raise NotImplementedError(f"This function is not implemented for {engine}") + + +def create_pretrained_project_pytorch( + project: str, + experimenter: str, + videos: list[str], + dataset: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | None = None, + analyze_video: bool = True, + filtered: bool = True, + create_labeled_video: bool = True, + train_fraction: float | None = None, + multi_animal: bool = False, + individuals: list[str] | None = None, + net_name: str | None = None, + detector_name: str | None = None, +): + 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. @@ -120,59 +273,285 @@ def create_pretrained_project( http://modelzoo.deeplabcut.org - Parameters - ---------- - project : string - String containing the name of the project. + Args: + project (string): String containing the name of the project. + experimenter (string): String containing the name of the experimenter. + dataset (string | None, optional): The superanimal dataset to use as basis for + the project. If not specified - superanimal_quadruped will be used by + default. Defaults to None. + videos (list[string]): A list of string containing the full paths of the videos + to include in the project. + working_directory (string, optional): The directory where the project will be + created. If None - the current working directory will be used. Defaults to + None. + copy_videos (bool, optional): If this is set to True, the videos are copied to + the ``videos`` directory. If it is False, symlink of the videos are copied + to the project/videos directory. Note: on Windows: True is often necessary! + Defaults to False. + analyze_video (bool, optional): If true, then the video is analyzed and a + labeled video is created. If false, then only the project will be created + and the weights downloaded. + filtered (bool, optional): Indicates if filtered pose data output should be + plotted rather than frame-by-frame predictions. Filtered version can be + calculated with deeplabcut.filterpredictions(). Defaults to True. + create_labeled_video (bool, optional): Specifies if a labeled video needs to be + created. Defaults to True. + train_fraction (float | None, optional): Fraction that will be used in + dlc-model/trainingset folder name. If None - default value (0.95) from new + projects will be used. Defaults to None. + multi_animal (bool, optional): Specifies if the project is single or + multi-animal. Defaults to False. + individuals (list[str] | None, optional): Only if multianimal is True. Defines + the names of the individuals. Defaults to None. + net_name (str | None, optional): Valid only if using Pytorch engine. Name of the + pose model on which the superanimal dataset has been trained on. If None - + "hrnet_w32" will be used as default. Defaults to None. + detector_name (str | None, optional): Valid only if using Pytorch engine. Name + of the detector model on which the superanimal dataset has been trained on. + If None - "fasterrcnn_resnet50_fpn_v2" will be used as default. Defaults to + None. + + Examples: + 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 + ) + + 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 + ) + + Windows: + + deeplabcut.create_pretrained_project_pytorch( + "humanstrokestudy", + "Bill", + [r"C:\yourusername\rig-95\Videos\reachingvideo1.avi"], + r"C:\yourusername\analysis\project", + copy_videos=True, + ) + + On Windows, paths should be formatted as ``r`"C:\"`` or ``"C:\\"`` (i.e. a double backslash). + """ + # Check arguments + if not dataset: + dataset = "superanimal_quadruped" + + if not net_name: + net_name = "hrnet_w32" + + # Currently, all Pytorch Superanimal models are Top-Down. + if not detector_name: + 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()}") + + if net_name not in get_available_models(dataset): + raise ValueError( + 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}. " + f"The following detectors are available: {get_available_detectors(dataset)}" + ) + + # Create project + cfg_path = deeplabcut.create_new_project( + project=project, + experimenter=experimenter, + videos=videos, + working_directory=working_directory, + copy_videos=copy_videos, + video_extensions=video_extensions, + multianimal=multi_animal, + individuals=individuals, + ) - experimenter : string - String containing the name of the experimenter. + # Edits to do to the project config + cfg_edits = {} + if train_fraction is not None: + cfg_edits["TrainingFraction"] = [train_fraction] + super_animal_project_cfg = get_super_animal_project_cfg(dataset) + super_animal_bodyparts = super_animal_project_cfg.get("bodyparts") + super_animal_skeleton = super_animal_project_cfg.get("skeleton") + cfg_edits["skeleton"] = super_animal_skeleton + if multi_animal: + cfg_edits["multianimalbodyparts"] = super_animal_bodyparts + else: + cfg_edits["bodyparts"] = super_animal_bodyparts + config = ProjectConfig.from_yaml(cfg_path) + config.update(cfg_edits) + config.to_yaml(cfg_path, log_changes=True, mark_clean=True) + + # Create the shuffle train and test directories + shuffle_dir = Path(cfg_path).parent / auxiliaryfunctions.get_model_folder( + trainFraction=config["TrainingFraction"][0], + shuffle=1, + cfg=config, + engine=Engine.PYTORCH, + ) + train_dir = shuffle_dir / "train" + test_dir = shuffle_dir / "test" + train_dir.mkdir(parents=True, exist_ok=True) + test_dir.mkdir(parents=True, exist_ok=True) + + # Download the weights and put them into appropriate directory + print("Downloading weights...") + super_animal_detector_name = f"{dataset}_{detector_name}" + new_detector_name = "snapshot-detector-000.pt" + download_huggingface_model( + model_name=super_animal_detector_name, + target_dir=str(train_dir), + rename_mapping={f"{super_animal_detector_name}.pt": new_detector_name}, + ) + super_animal_model_name = f"{dataset}_{net_name}" + new_snapshot_name = "snapshot-000.pt" + download_huggingface_model( + model_name=super_animal_model_name, + target_dir=str(train_dir), + rename_mapping={f"{super_animal_model_name}.pt": new_snapshot_name}, + ) + + # Create pytorch_config.yaml + train_cfg_path = train_dir / "pytorch_config.yaml" + pytorch_config = load_super_animal_config( + super_animal=dataset, + model_name=net_name, + detector_name=detector_name, + ) + pytorch_config["metadata"] = PoseMetadata.build(config, pose_config_path=train_cfg_path).to_dict() + 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.to_yaml(train_cfg_path) + + # 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) + + # Create inference_cfg.yaml if needed + if multi_animal: + inference_cfg_path = test_dir / "inference_cfg.yaml" + _create_inference_config(inference_cfg_path, config) + + # Create metadata.yaml with shuffle info in training-data directory + _create_training_datasets_metadata(config, shuffle_dir.name, Engine.PYTORCH) + + # Process the videos + _process_videos( + cfg_path=cfg_path, + video_extensions=video_extensions, + analyze_video=analyze_video, + filtered=filtered, + create_labeled_video=create_labeled_video, + ) + return cfg_path, str(train_cfg_path) - model: string, options see http://www.mousemotorlab.org/dlc-modelzoo - Current option and default: 'full_human' Creates a demo human project and analyzes a video with ResNet 101 weights pretrained on MPII Human Pose. This is from the DeeperCut paper - by Insafutdinov et al. https://arxiv.org/abs/1605.03170 Please make sure to cite it too if you use this code! - videos : list - A list of string containing the full paths of the videos to include in the project. +def _create_inference_config(inference_cfg_path: str | Path, project_cfg: dict): + inf_updates = dict( + minimalnumberofconnections=int(len(project_cfg["multianimalbodyparts"]) / 2), + topktoretain=len(project_cfg["individuals"]), + withid=project_cfg.get("identity", False), + ) + default_inf_path = auxiliaryfunctions.get_deeplabcut_path() / "inference_cfg.yaml" + MakeInference_yaml(inf_updates, inference_cfg_path, default_inf_path) + + +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") +def create_pretrained_project_tensorflow( + project: str, + experimenter: str, + videos: list[str], + model: str | None = None, + working_directory: str | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + analyzevideo: bool = True, + filtered: bool = True, + createlabeledvideo: bool = True, + trainFraction: float | None = None, +): + r"""Method used specifically for Tensorflow-based ModelZoo models. - 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. + Creates a new project directory, sub-directories and a basic configuration file. + Change its parameters to your projects need. - copy_videos : bool, optional ON WINDOWS: TRUE is often necessary! - If this is set to True, the videos are copied to the ``videos`` directory. If it is False,symlink of the videos are copied to the project/videos directory. The default is ``False``; if provided it must be either - ``True`` or ``False``. + The project will also be initialized with a pre-trained model from the DeepLabCut model zoo! - analyzevideo " bool, optional - If true, then the video is analzyed and a labeled video is created. If false, then only the project will be created and the weights downloaded. You can then access them + http://modelzoo.deeplabcut.org - filtered: bool, default false - Boolean variable indicating if filtered pose data output should be plotted rather than frame-by-frame predictions. - Filtered version can be calculated with deeplabcut.filterpredictions + Args: + project (string): String containing the name of the project. + experimenter (string): String containing the name of the experimenter. + model (string | None, optional): The model / dataset to use as basis for the + project. If not specified - full_human will be used by default. Defaults to + None. + videos (list[string]): A list of string containing the full paths of the videos + to include in the project. + working_directory (string, optional): The directory where the project will be + created. If None - the current working directory will be used. Defaults to + None. + copy_videos (bool, optional): If this is set to True, the videos are copied to + the ``videos`` directory. If it is False, symlink of the videos are copied + to the project/videos directory. Note: on Windows: True is often necessary! + Defaults to False. + analyzevideo (bool, optional): If true, then the video is analyzed and a labeled + video is created. If false, then only the project will be created and the + weights downloaded. + filtered (bool, optional): Indicates if filtered pose data output should be + plotted rather than frame-by-frame predictions. Filtered version can be + calculated with deeplabcut.filterpredictions(). Defaults to True. + createlabeledvideo (bool, optional): Specifies if a labeled video needs to be + created. Defaults to True. + trainFraction (float | None, optional): Fraction that will be used in + dlc-model/trainingset folder name. If None - default value (0.95) from new + projects will be used. Defaults to None. + + Examples: + 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 + ) - trainFraction: By default value from *new* projects. (0.95) - Fraction that will be used in dlc-model/trainingset folder name. + Loading full_cat model and analyzing video "felixfeliscatus3.avi": - Example - -------- - Linux/MacOs loading full_human model and analzying video /homosapiens1.avi - >>> deeplabcut.create_pretrained_project('humanstrokestudy','Linus',['/data/videos/homosapiens1.avi'], copy_videos=False) + deeplabcut.create_pretrained_project_tensorflow( + "humanstrokestudy", "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF + ) - Loading full_cat model and analzying video "felixfeliscatus3.avi" - >>> deeplabcut.create_pretrained_project('humanstrokestudy','Linus',['/data/videos/felixfeliscatus3.avi'], model='full_cat') + Windows: - Windows: - >>> 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 \ \ ) + deeplabcut.create_pretrained_project_tensorflow( + "humanstrokestudy", + "Bill", + [r"C:\yourusername\rig-95\Videos\reachingvideo1.avi"], + r"C:\yourusername\analysis\project", + copy_videos=True, + ) + On Windows, paths should be formatted as ``r`"C:\"`` or ``"C:\\"`` (i.e. a double backslash). """ - if model in globals()["Modeloptions"]: - cwd = os.getcwd() + if not model: + model = "full_human" + + if model in MODELOPTIONS: + cwd = Path.cwd() cfg = deeplabcut.create_new_project( - project, experimenter, videos, working_directory, copy_videos, videotype + project, experimenter, videos, working_directory, copy_videos, video_extensions=video_extensions ) if trainFraction is not None: - auxiliaryfunctions.edit_config(cfg, {"TrainingFraction": [trainFraction]}) + ProjectConfig.from_yaml(cfg).update(TrainingFraction=[trainFraction]).to_yaml( + cfg, log_changes=True, mark_clean=True + ) config = auxiliaryfunctions.read_config(cfg) if model == "full_human": @@ -215,87 +594,63 @@ def create_pretrained_project( auxiliaryfunctions.write_config(cfg, config) config = auxiliaryfunctions.read_config(cfg) - train_dir = Path( - os.path.join( - config["project_path"], - str( - auxiliaryfunctions.GetModelFolder( - trainFraction=config["TrainingFraction"][0], - shuffle=1, - cfg=config, - ) - ), - "train", + train_dir = ( + Path(config["project_path"]) + / str( + auxiliaryfunctions.get_model_folder( + trainFraction=config["TrainingFraction"][0], + shuffle=1, + cfg=config, + ) ) + / "train" ) - test_dir = Path( - os.path.join( - config["project_path"], - str( - auxiliaryfunctions.GetModelFolder( - trainFraction=config["TrainingFraction"][0], - shuffle=1, - cfg=config, - ) - ), - "test", + test_dir = ( + Path(config["project_path"]) + / str( + auxiliaryfunctions.get_model_folder( + trainFraction=config["TrainingFraction"][0], + shuffle=1, + cfg=config, + ) ) + / "test" ) # Create the model directory train_dir.mkdir(parents=True, exist_ok=True) test_dir.mkdir(parents=True, exist_ok=True) - modelfoldername = auxiliaryfunctions.GetModelFolder( + 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(Path(config["project_path"]) / Path(modelfoldername) / "train" / "pose_cfg.yaml") + path_test_config = str(Path(config["project_path"]) / Path(modelfoldername) / "test" / "pose_cfg.yaml") # Download the weights and put then in appropriate directory - print("Dowloading weights...") - auxfun_models.DownloadModel(model, train_dir) + print("Downloading weights...") + download_huggingface_model(model, train_dir) pose_cfg = deeplabcut.auxiliaryfunctions.read_plainconfig(path_train_config) + pose_cfg["dataset_type"] = "imgaug" print(path_train_config) # Updating config file: - dict = { + dict_ = { "default_net_type": pose_cfg["net_type"], "default_augmenter": pose_cfg["dataset_type"], "bodyparts": pose_cfg["all_joints_names"], - "skeleton": [], # TODO: update with paf_graph "dotsize": 6, } - auxiliaryfunctions.edit_config(cfg, dict) - - # Create the pose_config.yaml files - parent_path = Path(os.path.dirname(deeplabcut.__file__)) - defaultconfigfile = str(parent_path / "pose_cfg.yaml") - trainingsetfolder = auxiliaryfunctions.GetTrainingSetFolder(config) - datafilename, metadatafilename = auxiliaryfunctions.GetDataandMetaDataFilenames( - trainingsetfolder, - trainFraction=config["TrainingFraction"][0], - shuffle=1, - cfg=config, - ) + ProjectConfig.from_yaml(cfg).update(dict_).to_yaml(cfg, log_changes=True, mark_clean=True) - # downloading base encoder / not required unless on re-trains (but when a training set is created this happens anyway) - # model_path, num_shuffles=auxfun_models.Check4weights(pose_cfg['net_type'], parent_path, num_shuffles= 1) + # 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: - snapshotname = [fn for fn in os.listdir(train_dir) if ".meta" in fn][0].split( - ".meta" - )[0] + snapshotname = [p.name for p in Path(train_dir).iterdir() if ".meta" in p.name][0].split(".meta")[0] dict2change = { - "init_weights": str(os.path.join(train_dir, snapshotname)), + "init_weights": str(Path(train_dir) / snapshotname), "project_path": str(config["project_path"]), } @@ -315,23 +670,63 @@ def create_pretrained_project( MakeTest_pose_yaml(pose_cfg, keys2save, path_test_config) - video_dir = os.path.join(config["project_path"], "videos") - if analyzevideo == True: - print("Analyzing video...") - deeplabcut.analyze_videos(cfg, [video_dir], videotype, save_as_csv=True) + _create_training_datasets_metadata(config, modelfoldername.name, Engine.TF) - if createlabeledvideo == True: - if filtered: - deeplabcut.filterpredictions(cfg, [video_dir], videotype) - - print("Plotting results...") - deeplabcut.create_labeled_video( - cfg, [video_dir], videotype, draw_skeleton=True, filtered=filtered - ) - deeplabcut.plot_trajectories(cfg, [video_dir], videotype, filtered=filtered) + _process_videos( + cfg_path=cfg, + video_extensions=video_extensions, + analyze_video=analyzevideo, + filtered=filtered, + create_labeled_video=createlabeledvideo, + ) os.chdir(cwd) return cfg, path_train_config else: return "N/A", "N/A" + + +def _create_training_datasets_metadata(config: dict, shuffle_dir_name: str, engine: Engine): + # First create the metadata object + metadata = TrainingDatasetMetadata.create(config) + + # Create a new shuffle with TensorFlow engine + new_shuffle = ShuffleMetadata( + name=shuffle_dir_name, + train_fraction=config["TrainingFraction"][0], + index=1, + engine=engine, + split=DataSplit(train_indices=(), test_indices=()), + ) + + # Add the shuffle to metadata + metadata = metadata.add(new_shuffle) + + # Save the metadata + metadata.save() + + return metadata + + +def _process_videos( + cfg_path: str | Path, + video_extensions: str | Sequence[str] | None = None, + analyze_video: bool = True, + filtered: bool = True, + create_labeled_video: bool = True, +): + cfg_path = str(cfg_path) + video_dir = Path(cfg_path).parent / "videos" + + if analyze_video: + print("Analyzing video...") + deeplabcut.analyze_videos(cfg_path, [video_dir], video_extensions=video_extensions, save_as_csv=True) + + if create_labeled_video: + if filtered: + deeplabcut.filterpredictions(cfg_path, [video_dir], video_extensions) + + print("Plotting results...") + deeplabcut.create_labeled_video(cfg_path, [video_dir], video_extensions, draw_skeleton=True, filtered=filtered) + deeplabcut.plot_trajectories(cfg_path, [video_dir], video_extensions, filtered=filtered) diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index 849de32b9b..2f3400e427 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -1,179 +1,221 @@ -""" -DeepLabCut2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut -Please see AUTHORS for contributors. +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" import os import shutil import warnings +from collections.abc import Sequence from pathlib import Path +from typing import Literal + from deeplabcut import DEBUG -from deeplabcut.utils.auxfun_videos import VideoReader +from deeplabcut.core.deprecation import renamed_parameter +from deeplabcut.core.engine import Engine +from deeplabcut.utils.auxfun_videos import VideoReader, collect_video_paths +@renamed_parameter(old="videotype", new="video_extensions", since="3.0.0") def create_new_project( - project, - experimenter, - videos, - working_directory=None, - copy_videos=False, - videotype=".avi", - multianimal=False, -): - """Creates a new project directory, sub-directories and a basic configuration file. The configuration file is loaded with the default values. Change its parameters to your projects need. - - Parameters - ---------- - project : string - String containing the name of the project. - - experimenter : string - String containing the name of the experimenter. - - videos : list - A list of string containing the full paths of the videos to include in the project. - Attention: Can also be a directory, then all videos of videotype will be imported. - - 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. - - copy_videos : bool, optional - If this is set to True, the videos are copied to the ``videos`` directory. If it is False,symlink of the videos are copied to the project/videos directory. The default is ``False``; if provided it must be either - ``True`` or ``False``. - - multianimal: bool, optional. Default: False. - For creating a multi-animal project (introduced in DLC 2.2) - - Example - -------- - Linux/MacOs - >>> deeplabcut.create_new_project('reaching-task','Linus',['/data/videos/mouse1.avi','/data/videos/mouse2.avi','/data/videos/mouse3.avi'],'/analysis/project/') - >>> deeplabcut.create_new_project('reaching-task','Linus',['/data/videos'],videotype='.mp4') - - Windows: - >>> deeplabcut.create_new_project('reaching-task','Bill',[r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], copy_videos=True) - Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) + project: str, + experimenter: str, + videos: list[str | Path], + working_directory: str | Path | None = None, + copy_videos: bool = False, + video_extensions: str | Sequence[str] | None = None, + multianimal: bool = False, + individuals: list[str] | None = None, +) -> Path | Literal["nothingcreated"]: + r"""Create the necessary folders and files for a new project. + + Creating a new project involves creating the project directory, sub-directories and + a basic configuration file. The configuration file is loaded with the default + values. Change its parameters to your projects need. + + Args: + project (string): The name of the project. + experimenter (string): The name of the experimenter. + videos (list[str | Path]): A list of strings or paths representing the full + paths of the videos or video-directories to include in the project. If a + path represents a directory instead of a file, all videos of + ``video_extensions`` will be imported. + working_directory (str | Path | None, optional): The directory where the + project will be created. The default is the ``current working directory``. + copy_videos (bool, optional): If True, the videos are copied to the ``videos`` + directory. If False, symlinks of the videos will be created in the + ``project/videos`` directory; in the event of a failure to create symbolic + links, videos will be moved instead. Defaults to False. + video_extensions (str | Sequence[str] | None, optional): + Controls how ``videos`` are filtered, based on file extension. + File paths and directory contents are treated differently: + - ``None`` (default): file paths are accepted as-is; directories are + scanned for files with a recognized video extension. + - ``str`` or ``Sequence[str]`` (e.g. ``"mp4"`` or ``["mp4", "avi"]``): + both file paths and directory contents are filtered by the given + extension(s). Defaults to None. + multianimal (bool, optional): For creating a multi-animal project (introduced + in DLC 2.2). Defaults to False. + individuals (list[str] | None, optional): Relevant only if multianimal is True. + List of individuals to be used in the project configuration. If None, + defaults to ['individual1', 'individual2', 'individual3']. + + Returns: + Path | Literal["nothingcreated"]: Path to the new project configuration file, + or ``"nothingcreated"`` if no valid videos were found. + + Raises: + FileNotFoundError: If a non-existent path is passed to ``videos``. + + Examples: + Linux/MacOS: + + deeplabcut.create_new_project( + project='reaching-task', + experimenter='Linus', + videos=[ + '/data/videos/mouse1.avi', + '/data/videos/mouse2.avi', + '/data/videos/mouse3.avi' + ], + working_directory='/analysis/project/', + ) + deeplabcut.create_new_project( + project='reaching-task', + experimenter='Linus', + videos=['/data/videos'], + video_extensions='.mp4', + ) + + Windows: + + deeplabcut.create_new_project( + 'reaching-task', + 'Bill', + [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + copy_videos=True, + ) + On Windows, paths should be formatted as ``r`"C:\"`` or ``"C:\\"`` (i.e. a double backslash). """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions + months_3letter = { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + } + date = dt.today() - month = date.strftime("%B") + month = months_3letter[date.month] day = date.day d = str(month[0:3] + str(day)) date = dt.today().strftime("%Y-%m-%d") - if working_directory == None: + if working_directory is None: working_directory = "." - wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}".format(pn=project, exp=experimenter, date=date) + wd = Path(working_directory).absolute() + 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)) - return + print(f'Project "{project_path}" already exists!') + return (project_path / "config.yaml").absolute() video_path = project_path / "videos" data_path = project_path / "labeled-data" shuffles_path = project_path / "training-datasets" 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)) - - # 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! - vids = [] - for i in videos: - # Check if it is a folder - if os.path.isdir(i): - vids_in_dir = [ - os.path.join(i, vp) for vp in os.listdir(i) if videotype in vp - ] - vids = vids + vids_in_dir - if len(vids_in_dir) == 0: - print("No videos found in", i) - print( - "Perhaps change the videotype, which is currently set to:", - videotype, - ) - else: - videos = vids - print( - len(vids_in_dir), - " videos from the directory", - i, - "were added to the project.", - ) - else: - if os.path.isfile(i): - vids = vids + [i] - videos = vids + 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! + collected_videos: list[Path] = collect_video_paths(videos, extensions=video_extensions) - videos = [Path(vp) for vp in videos] - dirs = [data_path / Path(i.stem) for i in videos] + # TODO @deruyter92 2026-05-20: Move this verbosity block to `collect_video_paths` instead + files_per_dir: dict[Path, int] = {} + for f in collected_videos: + files_per_dir[f.parent] = files_per_dir.get(f.parent, 0) + 1 + for dir, count in files_per_dir.items(): + print(f"Found {count} videos in {dir}") + for p in (Path(v) for v in videos if Path(v).is_dir()): + if p.absolute() not in {d.absolute() for d in files_per_dir}: + print(f"No videos found in {p}") + print(f"Perhaps change the video_extensions, which is currently set to: {video_extensions}") + + 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] - if copy_videos == True: + 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/ + 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) + dst.symlink_to(src) + print(f"Created the symlink of {src} to {dst}") except OSError: - import subprocess + try: + import subprocess - subprocess.check_call("mklink %s %s" % (dst, src), shell=True) - print("Created the symlink of {} to {}".format(src, dst)) + subprocess.check_call(f"mklink {os.fspath(dst)} {os.fspath(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(f"{src} copied to {dst}") videos = destinations - if copy_videos == True: - videos = ( - destinations - ) # in this case the *new* location should be added to the config file + if copy_videos: + videos = destinations # in this case the *new* location should be added to the config file # adds the video list to the config.yaml file video_sets = {} 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)] - rel_video_path = str(Path.resolve(Path(video))) - except: - rel_video_path = os.readlink(str(video)) + video_key = Path(video).absolute() 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...") - os.remove(video) # Removing the video or link from the project + vid = VideoReader(os.fspath(video_key)) + video_sets[os.fspath(video_key)] = {"crop": ", ".join(map(str, vid.get_bbox()))} + except OSError: + warnings.warn("Cannot open the video file! Skipping to the next one...", stacklevel=2) + Path(video).unlink() # Removing the video or link from the project if not len(video_sets): # Silently sweep the files that were already written. 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" @@ -182,7 +224,7 @@ def create_new_project( cfg_file, ruamelFile = auxiliaryfunctions.create_config_template(multianimal) cfg_file["multianimalproject"] = multianimal cfg_file["identity"] = False - cfg_file["individuals"] = ["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!" @@ -191,8 +233,16 @@ def create_new_project( ["bodypart2", "bodypart3"], ["bodypart1", "bodypart3"], ] - cfg_file["default_augmenter"] = "multi-animal-imgaug" - cfg_file["default_net_type"] = "dlcrnet_ms5" + engine = cfg_file.get("engine") + if engine in Engine.PYTORCH.aliases: + cfg_file["default_augmenter"] = "albumentations" + cfg_file["default_net_type"] = "resnet_50" + elif engine in Engine.TF.aliases: + cfg_file["default_augmenter"] = "multi-animal-imgaug" + cfg_file["default_net_type"] = "dlcrnet_ms5" + else: + raise ValueError(f"Unknown or undefined engine {engine}") + cfg_file["default_track_method"] = "ellipse" else: cfg_file, ruamelFile = auxiliaryfunctions.create_config_template() cfg_file["multianimalproject"] = False @@ -200,13 +250,12 @@ def create_new_project( cfg_file["skeleton"] = [["bodypart1", "bodypart2"], ["objectA", "bodypart3"]] cfg_file["default_augmenter"] = "default" cfg_file["default_net_type"] = "resnet_50" - cfg_file["croppedtraining"] = False # common parameters: cfg_file["Task"] = project cfg_file["scorer"] = experimenter cfg_file["video_sets"] = video_sets - cfg_file["project_path"] = str(project_path) + cfg_file["project_path"] = project_path cfg_file["date"] = d cfg_file["cropping"] = False cfg_file["start"] = 0 @@ -215,15 +264,15 @@ def create_new_project( cfg_file["TrainingFraction"] = [0.95] cfg_file["iteration"] = 0 cfg_file["snapshotindex"] = -1 + cfg_file["detector_snapshotindex"] = -1 cfg_file["x1"] = 0 cfg_file["x2"] = 640 cfg_file["y1"] = 277 cfg_file["y2"] = 624 - cfg_file[ - "batch_size" - ] = ( - 8 - ) # batch size during inference (video - analysis); see https://www.biorxiv.org/content/early/2018/10/30/457242 + cfg_file["batch_size"] = ( + 8 # batch size during inference (video - analysis); see https://www.biorxiv.org/content/early/2018/10/30/457242 + ) + cfg_file["detector_batch_size"] = 1 cfg_file["corner2move2"] = (50, 50) cfg_file["move2corner"] = True cfg_file["skeleton_color"] = "black" @@ -232,13 +281,17 @@ def create_new_project( cfg_file["alphavalue"] = 0.7 # for plots transparency of markers cfg_file["colormap"] = "rainbow" # for plots type of colormap - projconfigfile = os.path.join(str(project_path), "config.yaml") + projconfigfile = (project_path / "config.yaml").absolute() # Write dictionary to yaml config file auxiliaryfunctions.write_config(projconfigfile, cfg_file) 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 9eacb82b18..eb2919057e 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -1,49 +1,46 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# -Please see AUTHORS for contributors. -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" -import os from pathlib import Path from deeplabcut import DEBUG def create_new_project_3d(project, experimenter, num_cameras=2, working_directory=None): - """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 - ---------- - project : string - String containing the name of the project. + Args: + project (string): String containing the name of the project. + experimenter (string): String containing the name of the experimenter. + num_cameras (int): 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. - experimenter : string - String containing the name of the experimenter. + Examples: + Linux/MacOs: - num_cameras : int - An integer value specifying the number of cameras. + deeplabcut.create_new_project_3d("reaching-task", "Linus", 2) - 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. + Windows: + deeplabcut.create_new_project_3d("reaching-task", "Bill", 2) - Example - -------- - Linux/MacOs - >>> deeplabcut.create_new_project_3d('reaching-task','Linus',2) - - Windows: - >>> deeplabcut.create_new_project('reaching-task','Bill',2) - Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) - + On Windows, paths should be formatted as ``r`"C:\"`` or ``"C:\\"`` (i.e. a double backslash). """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions date = dt.today() @@ -52,17 +49,15 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director d = str(month[0:3] + str(day)) date = dt.today().strftime("%Y-%m-%d") - if working_directory == None: + if working_directory is None: working_directory = "." - wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}-{triangulate}".format( - pn=project, exp=experimenter, date=date, triangulate="3d" - ) + wd = Path(working_directory).absolute() + 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(): - print('Project "{}" already exists!'.format(project_path)) + print(f'Project "{project_path}" already exists!') return camera_matrix_path = project_path / "camera_matrix" @@ -79,7 +74,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() @@ -87,7 +82,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 @@ -96,9 +94,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"] = [ @@ -111,26 +107,21 @@ 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") + projconfigfile = project_path / "config.yaml" auxiliaryfunctions.write_config_3d(projconfigfile, cfg_file_3d) 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" + 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/__init__.py b/deeplabcut/generate_training_dataset/__init__.py index 528117c3e6..7729536aba 100644 --- a/deeplabcut/generate_training_dataset/__init__.py +++ b/deeplabcut/generate_training_dataset/__init__.py @@ -1,13 +1,20 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# -Please see AUTHORS for contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" from deeplabcut.generate_training_dataset.frame_extraction import * -from deeplabcut.generate_training_dataset.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 57261865f0..7f7b22da6f 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -1,36 +1,33 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut - -Please see AUTHORS for contributors. -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" - - -def select_cropping_area(config, videos=None): +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from pathlib import Path + + +def select_cropping_area(config: str | Path, 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. + + Args: + config (string): Full path of the config.yaml file as a string. + videos (list, optional): List of videos whose cropping areas are to be defined. + Full paths are required. By default, all videos in the config are loaded. + Defaults to None. + + Returns: + dict: Updated project configuration """ - 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 - ---------- - config : string - Full path of the config.yaml file as a string. - - videos : optional (default=None) - List of videos whose cropping areas are to be defined. Note that full paths are required. - By default, all videos in the config are successively loaded. - - Returns - ------- - 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: @@ -62,7 +59,7 @@ def select_cropping_area(config, videos=None): def extract_frames( - config, + config: str | Path, mode="automatic", algo="kmeans", crop=False, @@ -74,142 +71,193 @@ def extract_frames( slider_width=25, config3d=None, extracted_cam=0, + videos_list=None, ): - """ - 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_video`` at any stage of the project to add new videos to the config file and extract their frames. - - The provided function either selects frames from the videos in a randomly and temporally uniformly distributed way (uniform), \n - by clustering based on visual appearance (k-means), or by manual selection. - - Three important parameters for automatic extraction: numframes2pick, start and stop are set in the config file. - - After frames have been extracted from all videos from one camera, matched frames from other cameras can be extracted using mode = ``match``. - This is necessary if you plan to use epipolar lines to improve labeling across multiple camera angles. It will overwrite previously extracted - images from the second camera angle if necessary. - - Please refer to the user guide for more details on methods and parameters https://www.nature.com/articles/s41596-019-0176-0 - or the preprint: https://www.biorxiv.org/content/biorxiv/early/2018/11/24/476531.full.pdf - - Parameters - ---------- - config : string - Full path of the config.yaml file as a string. - - mode : string - String containing the mode of extraction. It must be either ``automatic`` or ``manual`` to extract the inital set of frames. It can also be ``match`` to match frames between - the cameras in preparation for the use of epipolar lines during labeling; namely, extract from camera_1 first, then run this to extact the matched frames in camera_2. - WARNING: if you use match, and you previously extracted and labeled frames from the second camera, this will overwrite your data. This will require you deleting the - collectdata.h5/.csv files before labeling.... Use with caution! - - algo : string - String specifying the algorithm to use for selecting the frames. Currently, deeplabcut supports either ``kmeans`` or ``uniform`` based selection. This flag is - only required for ``automatic`` mode and the default is ``uniform``. For uniform, frames are picked in temporally uniform way, kmeans performs clustering on downsampled frames (see user guide for details). - Note: color information is discarded for kmeans, thus e.g. for camouflaged octopus clustering one might want to change this. - - crop : bool, optional - If True, video frames are cropped according to the corresponding coordinates stored in the config.yaml. - Alternatively, if cropping coordinates are not known yet, crop='GUI' triggers a user interface - where the cropping area can be manually drawn and saved. - - 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 - some folders and want to extract data for new videos. - - cluster_resizewidth: number, default: 30 - For k-means one can change the width to which the images are downsampled (aspect ratio is fixed). - - cluster_step: number, default: 1 - By default each frame is used for clustering, but for long videos one could only use every nth frame (set by: cluster_step). This saves memory before clustering can start, however, - reading the individual frames takes longer due to the skipping. - - cluster_color: bool, default: False - If false then each downsampled image is treated as a grayscale vector (discarding color information). If true, then the color channels are considered. This increases - the computational complexity. - - opencv: bool, default: True - Uses openCV for loading & extractiong (otherwise moviepy (legacy)) - - slider_width: number, default: 25 - Width of the video frames slider, in percent of window - - config3d: string, optional - Path to the config.yaml file in the 3D project. This will be used to match frames extracted from all cameras present in the field 'camera_names' to the - frames extracted from the camera given by the parameter 'extracted_cam' - - extracted_cam: number, default: 0 - The index of the camera that already has extracted frames. This will match frame numbers to extract for all other cameras. - This parameter is necessary if you wish to use epipolar lines in the labeling toolbox. Only use if mode = 'match' and config3d is provided. - - Examples - -------- - for selecting frames automatically with 'kmeans' and want to crop the frames. - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','automatic','kmeans',True) - -------- - for selecting frames automatically with 'kmeans' and defining the cropping area at runtime. - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','automatic','kmeans','GUI') - -------- - for selecting frames automatically with 'kmeans' and considering the color information. - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','automatic','kmeans',cluster_color=True) - -------- - for selecting frames automatically with 'uniform' and want to crop the frames. - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','automatic',crop=True) - -------- - for selecting frames manually, - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','manual') - -------- - for selecting frames manually, with a 60% wide frames slider - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml','manual', slider_width=60) - -------- - for extracting frames from a second camera that match the frames extracted from the first - >>> deeplabcut.extract_frames('/analysis/project/reaching-task/config.yaml', mode='match', extracted_cam=0) - - While selecting the frames manually, you do not need to specify the ``crop`` parameter in the command. Rather, you will get a prompt in the graphic user interface to choose - if you need to crop or not. - -------- + """Extracts frames from the project videos. + + Frames will be extracted from videos listed in the config.yaml file. + + The frames are selected from the videos in a randomly and temporally uniformly + distributed way (``uniform``), by clustering based on visual appearance + (``k-means``), or by manual selection. + + After frames have been extracted from all videos from one camera, matched frames + from other cameras can be extracted using ``mode = "match"``. This is necessary if + you plan to use epipolar lines to improve labeling across multiple camera angles. + It will overwrite previously extracted images from the second camera angle if + necessary. + + Please refer to the user guide for more details on methods and parameters + https://www.nature.com/articles/s41596-019-0176-0 or the preprint: + https://www.biorxiv.org/content/biorxiv/early/2018/11/24/476531.full.pdf + + Args: + config (string): Full path of the config.yaml file as a string. + mode (string): Either ``"automatic"``, ``"manual"`` or ``"match"``. + String containing the mode of extraction. It must be either ``"automatic"`` or + ``"manual"`` to extract the initial set of frames. It can also be ``"match"`` + to match frames between the cameras in preparation for the use of epipolar line + during labeling; namely, extract from camera_1 first, then run this to extract + the matched frames in camera_2. + + WARNING: if you use ``"match"``, and you previously extracted and labeled + frames from the second camera, this will overwrite your data. This will require + you to delete the ``collectdata(.h5/.csv)`` files before labeling. Use with + caution! + algo (string): Either ``"kmeans"`` or ``"uniform"``. Defaults to ``"kmeans"``. + String specifying the algorithm to use for selecting the frames. Currently, + deeplabcut supports either ``kmeans`` or ``uniform`` based selection. This flag + is only required for ``automatic`` mode and the default is ``kmeans``. For + ``"uniform"``, frames are picked in temporally uniform way, ``"kmeans"`` + performs clustering on downsampled frames (see user guide for details). + + NOTE: Color information is discarded for ``"kmeans"``, thus e.g. for + camouflaged octopus clustering one might want to change this. + crop (bool or str, optional): If ``True``, video frames are cropped according to the corresponding + coordinates stored in the project configuration file. Alternatively, if + cropping coordinates are not known yet, crop=``"GUI"`` triggers a user + interface where the cropping area can be manually drawn and saved. + 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 + some folders and want to extract data for new videos. Defaults to True. + cluster_resizewidth (int): For ``"k-means"`` one can change the width to which the images are downsampled + (aspect ratio is fixed). Defaults to 30. + cluster_step (int): By default each frame is used for clustering, but for long videos one could + only use every nth frame (set using this parameter). This saves memory before + clustering can start, however, reading the individual frames takes longer due + to the skipping. Defaults to 1. + cluster_color (bool): If ``"False"`` then each downsampled image is treated as a grayscale vector + (discarding color information). If ``"True"``, then the color channels are + considered. This increases the computational complexity. Defaults to False. + opencv (bool): Uses openCV for loading and extracting (otherwise moviepy (legacy)). Defaults to True. + slider_width (int): Width of the video frames slider, in percent of window. Defaults to 25. + config3d (string, optional): Path to the project configuration file in the 3D project. This will be used to + match frames extracted from all cameras present in the field 'camera_names' to + the frames extracted from the camera given by the parameter 'extracted_cam'. + extracted_cam (int): The index of the camera that already has extracted frames. This will match + frame numbers to extract for all other cameras. This parameter is necessary if + you wish to use epipolar lines in the labeling toolbox. Only use if + ``mode='match'`` and ``config3d`` is provided. Defaults to 0. + videos_list (list[str]): A list of the string containing full paths to videos to extract frames for. If + this is left as ``None`` all videos specified in the config file will have + frames extracted. Otherwise one can select a subset by passing those paths. Defaults to None. + + Returns: + None + + Note: + Use the function ``add_new_videos`` at any stage of the project to add new videos + to the config file and extract their frames. + + The following parameters for automatic extraction are used from the config file + + * ``numframes2pick`` + * ``start`` and ``stop`` + + While selecting the frames manually, you do not need to specify the ``crop`` + parameter in the command. Rather, you will get a prompt in the graphic user + interface to choose if you need to crop or not. + + Examples: + To extract frames automatically with 'kmeans' and then crop the frames + + deeplabcut.extract_frames( + config='/analysis/project/reaching-task/config.yaml', + mode='automatic', + algo='kmeans', + crop=True, + ) + + To extract frames automatically with 'kmeans' and then defining the cropping area + using a GUI + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', + 'automatic', + 'kmeans', + 'GUI', + ) + To consider the color information when extracting frames automatically with + 'kmeans': + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', + 'automatic', + 'kmeans', + cluster_color=True, + ) + + To extract frames automatically with 'uniform' and then crop the frames + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', + 'automatic', + 'uniform', + crop=True, + ) + + To extract frames manually + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', 'manual' + ) + + To extract frames manually, with a 60% wide frames slider + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', 'manual', slider_width=60, + ) + + To extract frames from a second camera that match the frames extracted from the + first + + deeplabcut.extract_frames( + '/analysis/project/reaching-task/config.yaml', + mode='match', + extracted_cam=0, + ) """ - 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) + cfg = auxiliaryfunctions.read_config(config_file) + print("Config file read successfully.") + + if videos_list is None: + videos = list(cfg.get("video_sets_original") or cfg["video_sets"]) + else: # filter video_list by the ones in the config file + videos = [v for v in cfg["video_sets"] if v in videos_list] if mode == "manual": - wd = Path(config).resolve().parents[0] - os.chdir(str(wd)) - from deeplabcut.gui import frame_extraction_toolbox + from deeplabcut.gui.widgets import launch_napari - frame_extraction_toolbox.show(config, slider_width) + _ = launch_napari(videos[0]) + return elif mode == "automatic": - config_file = Path(config).resolve() - cfg = auxiliaryfunctions.read_config(config_file) - print("Config file read successfully.") - numframes2pick = cfg["numframes2pick"] start = cfg["start"] stop = cfg["stop"] # 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.") - videos = cfg.get("video_sets_original") or cfg["video_sets"] if opencv: - from deeplabcut.utils.auxfun_videos import VideoReader + from deeplabcut.utils.auxfun_videos import VideoWriter else: from moviepy.editor import VideoFileClip @@ -233,9 +281,8 @@ def extract_frames( or askuser == "oui" or askuser == "ouais" ): # multilanguage support :) - if opencv: - cap = VideoReader(video) + cap = VideoWriter(video) nframes = len(cap) else: # Moviepy: @@ -252,17 +299,12 @@ def extract_frames( output_path = Path(config).parents[0] / "labeled-data" / fname.stem if output_path.exists(): - if len(os.listdir(output_path)): + if any(output_path.iterdir()): if userfeedback: 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": @@ -272,26 +314,25 @@ def extract_frames( except KeyError: coords = cfg["video_sets_original"][video]["crop"].split(",") - if crop and not opencv: - clip = clip.crop( - y1=int(coords[2]), - y2=int(coords[3]), - x1=int(coords[0]), - x2=int(coords[1]), - ) - elif not crop: + if crop: + if opencv: + cap.set_bbox(*map(int, coords)) + else: + clip = clip.crop( + y1=int(coords[2]), + y2=int(coords[3]), + x1=int(coords[0]), + x2=int(coords[1]), + ) + 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 - ) + 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( @@ -299,8 +340,6 @@ def extract_frames( numframes2pick, start, stop, - crop, - coords, step=cluster_step, resizewidth=cluster_resizewidth, color=cluster_color, @@ -317,41 +356,26 @@ 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 + 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: for index in frames2pick: cap.set_to_frame(index) # extract a particular frame - frame = cap.read_frame() + 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" - ) - if crop: - io.imsave( - img_name, - image[ - int(coords[2]) : int(coords[3]), - int(coords[0]) : int(coords[1]), - :, - ], - ) # y1 = int(coords[2]),y2 = int(coords[3]),x1 = int(coords[0]), x2 = int(coords[1] - else: - io.imsave(img_name, image) + img_name = str(output_path) + "/img" + str(index).zfill(indexlength) + ".png" + io.imsave(img_name, image) is_valid.append(True) else: print("Frame", index, " not found!") @@ -361,16 +385,12 @@ 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( - "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: @@ -389,43 +409,44 @@ def extract_frames( if all(has_failed): print("Frame extraction failed. Video files must be corrupted.") - return + return has_failed elif any(has_failed): print("Although most frames were extracted, some were invalid.") else: - print("Frames were successfully extracted, for the videos of interest.") + 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' " - "(if you extracted enough frames for all videos)." + "(Note, you should label frames extracted from diverse videos " + "(and many videos; we do not recommend training on single videos!))." ) + return has_failed elif mode == "match": import cv2 - config_file = Path(config).resolve() + config_file = Path(config) cfg = auxiliaryfunctions.read_config(config_file) print("Config file read successfully.") videos = sorted(cfg["video_sets"].keys()) + if videos_list is not None: # filter video_list by the ones in the config file + 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/") + labels_path = project_path / "labeled-data" 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] - label_dirs = sorted( - glob.glob(os.path.join(labels_path, "*" + extCam_name + "*")) - ) + label_dirs = sorted(labels_path.glob("*" + extCam_name + "*")) # select crop method crop_list = [] for video in videos: - if extCam_name not in video: + if extCam_name in video: if crop == "GUI": cfg = select_cropping_area(config, [video]) print("in gui code") @@ -441,33 +462,38 @@ def extract_frames( elif not crop: coords = None crop_list.append(coords) - print(crop_list) - for coords, dirPath in zip(crop_list, label_dirs): - extracted_images = glob.glob(os.path.join(dirPath, "*png")) + for coords, dirPath in zip(crop_list, label_dirs, strict=False): + extracted_images = list(dirPath.glob("*png")) imgPattern = re.compile("[0-9]{1,10}") for cam in cams: - output_path = re.sub(extCam_name, cam, dirPath) - - for fname in os.listdir(output_path): - if fname.endswith(".png"): - os.remove(os.path.join(output_path, fname)) + output_path = Path(re.sub(extCam_name, cam, str(dirPath))) + + for p in output_path.iterdir(): + if p.name.endswith(".png"): + p.unlink() + + # Find the matching video from the config `video_sets`, + # as it may be stored elsewhere than in the `videos` directory. + video_name = output_path.name + vid = "" + for video in cfg["video_sets"]: + if video_name in video: + vid = video + break + if not vid: + raise ValueError(f"Video {video_name} not found...") - vid = os.path.join(video_dir, os.path.basename(output_path)) + ".avi" cap = cv2.VideoCapture(vid) - print( - "\n extracting matched frames from " - + os.path.basename(output_path) - + ".avi" - ) + print("\n extracting matched frames from " + video_name) for img in extracted_images: - imgNum = re.findall(imgPattern, os.path.basename(img))[0] + imgNum = re.findall(imgPattern, img.name)[0] cap.set(1, int(imgNum)) ret, frame = cap.read() if ret: image = img_as_ubyte(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - img_name = str(output_path) + "/img" + imgNum + ".png" + img_name = str(output_path / ("img" + imgNum + ".png")) if crop: io.imsave( img_name, @@ -479,12 +505,11 @@ 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( - "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/generate_training_dataset/metadata.py b/deeplabcut/generate_training_dataset/metadata.py new file mode 100644 index 0000000000..8e27d6463a --- /dev/null +++ b/deeplabcut/generate_training_dataset/metadata.py @@ -0,0 +1,495 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""File containing methods to load and parse shuffle metadata.""" + +from __future__ import annotations + +import logging +import pickle +import re +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + +from deeplabcut.core.config import ProjectConfig, get_yaml_dumper, get_yaml_loader +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + + +@dataclass(frozen=True) +class DataSplit: + """Class representing the metadata for a shuffle.""" + + train_indices: tuple[int, ...] + test_indices: tuple[int, ...] + + def __post_init__(self) -> None: + """ + Raises: + RuntimeError: If the indices are not sorted in strictly increasing order. + """ + for indices in [self.train_indices, self.test_indices]: + idx = np.array(indices) + if not np.all(idx[:-1] < idx[1:]): + raise RuntimeError( + "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 + engine: Engine + split: DataSplit | None + + def load_split( + self, + cfg: ProjectConfig | dict | Path | str, + trainset_path: Path, + ) -> ShuffleMetadata: + """Loads the data split for this shuffle. + + Args: + cfg (ProjectConfig | dict | Path | str): The project configuration. + trainset_path: the path to the training dataset folder + + Returns: + a new instance with the data split defined + """ + cfg = ProjectConfig.from_any(cfg) + _, doc_path = auxiliaryfunctions.get_data_and_metadata_filenames( + trainset_path, self.train_fraction, self.index, cfg + ) + if not doc_path.exists(): + raise ValueError( + f"Could not load the metadata file for {self} as {doc_path} does not " + f"exist. If you deleted the shuffle, you also need to delete the " + f"shuffle from metadata.yaml or recreate the metadata.yaml file." + ) + + with doc_path.open("rb") as f: + _, train_idx, test_idx, _ = pickle.load(f) + return ShuffleMetadata( + name=self.name, + train_fraction=self.train_fraction, + index=self.index, + engine=self.engine, + split=DataSplit( + train_indices=tuple(sorted([int(idx) for idx in train_idx])), + test_indices=tuple(sorted([int(idx) for idx in test_idx])), + ), + ) + + +@dataclass(frozen=True) +class TrainingDatasetMetadata: + """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 + iteration of a project), TrainingDatasetMetadata.create(cfg) should be called when + the "training-datasets" folder is still empty. + + For existing projects (created with DeepLabCut < 3.0), calling + TrainingDatasetMetadata.create(cfg) will go over documentation data for all existing + shuffles in the training-datasets folder and add them to a new metadata instance. + All shuffles will be given Engine.TF as an engine. + + Examples: + # Creating the metadata file for an existing project + config = "/data/my-dlc-project/config.yaml" + trainset_metadata = TrainingDatasetMetadata.create(config) + trainset_metadata.save() + + # Adding a new shuffle to the metadata file + config = "/data/my-dlc-project-2008-06-17/config.yaml" + trainset_metadata = TrainingDatasetMetadata.load(config) + new_shuffle = ShuffleMetadata( + name="my-dlc-projectJun17-trainset60shuffle5", + train_fraction=0.6, + index=5, + engine=compat.Engine.PYTORCH, + split=DataSplit(train_indices=(1, 3, 4), test_indices=(0, 2)), + ) + trainset_metadata = trainset_metadata.add(new_shuffle) + trainset_metadata.save() # saves to disk + """ + + project_config: ProjectConfig + shuffles: tuple[ShuffleMetadata, ...] + file_header: tuple[str] = ( + "# This file is automatically generated - DO NOT EDIT", + "# It contains the information about the shuffles created for the dataset", + "---", + ) + + def __post_init__(self) -> None: + """ + Raises: + RuntimeError: If the shuffles are not sorted in ascending training fraction and index. + """ + indices = [[s.train_fraction, s.index] for s in self.shuffles] + 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 " + f"fraction and index. Found {self.shuffles}" + ) + + def add( + self, + shuffle: ShuffleMetadata, + overwrite: bool = False, + ) -> TrainingDatasetMetadata: + """Adds a new shuffle to the metadata file. + + Args: + shuffle: the shuffle to add + overwrite: if a shuffle with the same index is already stored in the + metadata file, whether to overwrite it + + Returns: + A new instance of TrainingDatasetMetadata with updated shuffles + + Raises: + RuntimeError: If ``overwrite`` is False and a shuffle with the same index and + training fraction already exists. + """ + 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( + f"Cannot add {shuffle} to the meta: a shuffle with index " + f"{shuffle.index} and train_fraction {shuffle.train_fraction} " + f"already exists: {self.shuffles}." + ) + + existing_shuffles = [ + s for s in self.shuffles if (s.index != shuffle.index or s.train_fraction != shuffle.train_fraction) + ] + shuffles = existing_shuffles + [shuffle] + return TrainingDatasetMetadata( + project_config=self.project_config, + shuffles=tuple(sorted(shuffles, key=lambda s: (s.train_fraction, s.index))), + ) + + def get(self, trainset_index: int = 0, index: int = 0) -> ShuffleMetadata: + """ + Args: + trainset_index: the index of the trainset fraction as defined in config.yaml + index: the index of the shuffle + + Returns: + the shuffle with the given trainset index and shuffle index + + Raises: + ValueError: If trainset_index is out of bounds or the shuffle is not present + """ + fractions = self.project_config["TrainingFraction"] + if trainset_index >= len(fractions): + raise ValueError( + f"trainset_index={trainset_index} is out of bounds for " + f"TrainingFraction={fractions} (length {len(fractions)})." + ) + train_fraction = fractions[trainset_index] + for shuffle in self.shuffles: + if shuffle.train_fraction == train_fraction and shuffle.index == index: + return shuffle + + known = [(s.train_fraction, s.index) for s in self.shuffles] or "none" + raise ValueError( + f"Could not find a shuffle with train_fraction={train_fraction} and " + f"index={index}. Known shuffles (fraction, index): {known}." + ) + + def save(self) -> None: + """Saves the training dataset metadata to disk.""" + metadata = {"shuffles": {}} + data_splits: dict[DataSplit, int] = {} + trainset_path = self.path(self.project_config).parent + for s in self.shuffles: + if s.split is None: + s = s.load_split(cfg=self.project_config, trainset_path=trainset_path) + + split_index = data_splits.get(s.split) + if split_index is None: + split_index = len(data_splits) + 1 + data_splits[s.split] = split_index + + metadata["shuffles"][s.name] = { + "train_fraction": s.train_fraction, + "index": s.index, + "split": split_index, + "engine": s.engine.aliases[0], + } + + with self.path(self.project_config).open("w") as file: + file.write("\n".join(self.file_header) + "\n") + get_yaml_dumper().dump(metadata, file) + + @staticmethod + def load( + config: ProjectConfig | dict | Path | str, + load_splits: bool = False, + ) -> TrainingDatasetMetadata: + """Loads the metadata from disk. + + Args: + config (ProjectConfig | dict | Path | str): The project configuration. + load_splits: whether to load the data split for each shuffle + """ + cfg = ProjectConfig.from_any(config) + + metadata_path = TrainingDatasetMetadata.path(cfg) + if not metadata_path.exists(): + raise FileNotFoundError(f"No metadata.yaml found at {metadata_path}.") + with open(metadata_path) as file: + metadata = get_yaml_loader().load(file) + + shuffles = [] + for shuffle_name, shuffle_metadata in metadata["shuffles"].items(): + shuffle = ShuffleMetadata( + name=shuffle_name, + train_fraction=shuffle_metadata["train_fraction"], + index=shuffle_metadata["index"], + engine=Engine(shuffle_metadata["engine"]), + split=None, + ) + if load_splits: + shuffle = shuffle.load_split(cfg, metadata_path.parent) + + shuffles.append(shuffle) + + shuffles.sort(key=lambda s: (s.train_fraction, s.index)) + return TrainingDatasetMetadata(project_config=cfg, shuffles=tuple(shuffles)) + + @staticmethod + def create(config: ProjectConfig | dict | Path | str) -> TrainingDatasetMetadata: + """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. + + Args: + config (ProjectConfig | dict | Path | str): The project configuration. + default_engine: the default engine to set for shuffles in the project + + Returns: + the metadata for the existing shuffles in the project + """ + cfg = ProjectConfig.from_any(config) + + 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) + ] + else: + trainset_path.mkdir(parents=True, exist_ok=True) + shuffle_docs = [] + + prefix = cfg["Task"] + cfg["date"] + shuffles = [] + existing_splits: dict[tuple[tuple[int, ...], tuple[int, ...]], int] = {} + for doc_path in shuffle_docs: + index = int(doc_path.stem.split("shuffle")[-1]) + with doc_path.open("rb") as f: + _, train_idx, test_idx, train_frac = pickle.load(f) + + engine = Engine.TF + train_idx = tuple(sorted([int(idx) for idx in train_idx])) + test_idx = tuple(sorted([int(idx) for idx in test_idx])) + split_idx = existing_splits.get((train_idx, test_idx)) + if split_idx is None: + split_idx = len(existing_splits) + 1 + existing_splits[(train_idx, test_idx)] = split_idx + + shuffles.append( + ShuffleMetadata( + name=f"{prefix}-trainset{int(100 * train_frac)}shuffle{index}", + train_fraction=train_frac, + index=index, + engine=engine, + split=DataSplit(train_indices=train_idx, test_indices=test_idx), + ) + ) + + shuffles = tuple(sorted(shuffles, key=lambda s: (s.train_fraction, s.index))) + return TrainingDatasetMetadata( + project_config=cfg, + shuffles=shuffles, + ) + + @staticmethod + def path(cfg: ProjectConfig | dict | Path | str) -> Path: + """ + Args: + cfg (ProjectConfig | dict | Path | str): The project configuration. + + Returns: + the path to the training dataset metadata file + """ + cfg = ProjectConfig.from_any(cfg) + meta_path = auxiliaryfunctions.get_training_set_folder(cfg) / "metadata.yaml" + return Path(cfg["project_path"]) / meta_path + + +def update_metadata( + cfg: ProjectConfig | dict | Path | str, + train_fraction: float, + shuffle: int, + engine: Engine, + train_indices: list[int], + test_indices: list[int], + overwrite: bool = False, +) -> None: + """Updates the metadata for a training-dataset. + + Args: + cfg (ProjectConfig | dict | Path | str): The project configuration. + train_fraction: the train_fraction of the new shuffle + shuffle: the index of the shuffle to add + engine: the engine for the shuffle + train_indices: the indices of images in the training set + test_indices: the indices of images in the test set + overwrite: whether to overwrite a shuffle with the same index and train fraction + if one exists + + Raises: + RuntimeError: If ``overwrite`` is ``False`` and a shuffle with the same index and + training fraction already exists. + """ + cfg = ProjectConfig.from_any(cfg) + prefix = cfg["Task"] + cfg["date"] + metadata = TrainingDatasetMetadata.load(cfg, load_splits=True) + new_shuffle = ShuffleMetadata( + name=f"{prefix}-trainset{int(100 * train_fraction)}shuffle{shuffle}", + train_fraction=train_fraction, + index=shuffle, + engine=engine, + 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() + + +def get_shuffle_engine( + cfg: ProjectConfig | dict | Path | str, + trainingsetindex: int, + shuffle: int, + modelprefix: str = "", +) -> Engine: + """Get the shuffle engine. + + Args: + cfg (ProjectConfig | dict | Path | str): The project configuration. + trainingsetindex: the training set index used + shuffle: the shuffle for which to get the engine + modelprefix: the model prefix, if there is one + + Returns: + the engine that the shuffle was created with + + Raises: + ValueError: If the engine for the shuffle cannot be determined or the shuffle + doesn't exist + """ + cfg = ProjectConfig.from_any(cfg) + if not TrainingDatasetMetadata.path(cfg).exists(): + metadata = TrainingDatasetMetadata.create(cfg) + if metadata.shuffles: + # only persist when there is actual content to avoid writing empty files + metadata.save() + else: + metadata = TrainingDatasetMetadata.load(cfg) + + # Try to resolve the shuffle from metadata; fall through to model-folder detection + # on failure so that inference works even when metadata is incomplete. + shuffle_metadata = None + try: + shuffle_metadata = metadata.get(trainingsetindex, shuffle) + except ValueError as e: + logging.warning( + "Could not read shuffle metadata for trainingsetindex=%s, shuffle=%s: %s. " + "Falling back to detecting the engine from model folders.", + trainingsetindex, + shuffle, + e, + ) + + if shuffle_metadata is not None: + return shuffle_metadata.engine + + engines = find_engines_from_model_folders(cfg, trainingsetindex, shuffle, modelprefix) + if len(engines) == 0: + prefix_str = f" and modelprefix={modelprefix}" if modelprefix else "" + raise ValueError( + f"Couldn't find any shuffles with trainingsetindex={trainingsetindex}, " + f"shuffle={shuffle}{prefix_str}. The shuffle was not found " + "in metadata.yaml and no model folder exists for it. Please check that " + "such a shuffle is defined." + ) + + engine = list(engines)[0] # Get any engine from the set + if len(engines) > 1: + logging.warning( + f"Found multiple engines for trainingsetindex={trainingsetindex}, " + f"shuffle={shuffle} and modelprefix={modelprefix}. Using engine={engine}. " + f"To select another engine, please specify it in your API call." + ) + return engine + + +def find_engines_from_model_folders( + cfg: dict, + trainingsetindex: int, + shuffle: int, + modelprefix: str = "", +) -> set[Engine]: + """Determines which engines are used with a given shuffle. + + This method can be useful when using modelprefix, as the engine for a shuffle stored + under a "modelprefix" might not be the same as the base shuffle (for which the + engine is stored in the training-datasets folder). + + Args: + cfg: the config for the DeepLabCut project + trainingsetindex: the training set index used + shuffle: the shuffle for which to get the engine + modelprefix: the model prefix, if there is one + + Returns: + the engines for which a model folder exists for the given shuffle + """ + project_path = Path(cfg["project_path"]) + train_fraction = cfg["TrainingFraction"][trainingsetindex] + + existing_engines = set() + for engine in Engine: + expected_model_folder = project_path / auxiliaryfunctions.get_model_folder( + trainFraction=train_fraction, + shuffle=shuffle, + cfg=cfg, + engine=engine, + modelprefix=modelprefix, + ) + if expected_model_folder.exists(): + existing_engines.add(engine) + + return existing_engines diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 3abf0c8306..99fe05856b 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -1,114 +1,320 @@ -""" -DeepLabCut2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut - -Please see AUTHORS for contributors. -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations import os -import os.path +import re +import warnings from itertools import combinations from pathlib import Path import numpy as np from tqdm import tqdm -from deeplabcut.generate_training_dataset import trainingsetmanipulation -from deeplabcut.utils import auxiliaryfunctions, auxfun_models, auxfun_multianimal +import deeplabcut.compat as compat +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.config import ProjectConfig +from deeplabcut.core.engine import Engine +from deeplabcut.core.weight_init import WeightInitialization +from deeplabcut.utils import ( + auxfun_models, + auxfun_multianimal, + auxiliaryfunctions, +) + +from .trainingsetmanipulation import ( + MakeInference_yaml, + MakeTest_pose_yaml, + MakeTrain_pose_yaml, + SplitTrials, + merge_annotateddatasets, + pad_train_test_indices, + read_image_shape_fast, + validate_shuffles, +) + + +def format_multianimal_training_data( + df, + train_inds, + project_path, + n_decimals=2, +): + train_data = [] + nrows = df.shape[0] + filenames = df.index.to_list() + n_bodyparts = df.columns.get_level_values("bodyparts").unique().size + individuals = df.columns.get_level_values("individuals") + 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[..., 0] = np.arange(n_bodyparts) + temp = df.to_numpy() + temp_multi = temp[:, ~mask_single].reshape((nrows, n_animals, -1, 2)) + n_multibodyparts = temp_multi.shape[2] + array[:, :n_animals, :n_multibodyparts, 1:] = temp_multi + if n_animals != n_individuals: # There is a unique individual + n_uniquebodyparts = n_bodyparts - n_multibodyparts + temp_single = np.reshape(temp[:, mask_single], (nrows, 1, n_uniquebodyparts, 2)) + array[:, -1:, -n_uniquebodyparts:, 1:] = temp_single + array = np.round(array, decimals=n_decimals) + for i in tqdm(train_inds): + filename = filenames[i] + img_shape = read_image_shape_fast(Path(project_path).joinpath(*filename)) + joints = dict() + has_data = False + for n, xy in enumerate(array[i]): + # Drop missing body parts + xy = xy[~np.isnan(xy).any(axis=1)] + # Drop points lying outside the image + inside = np.logical_and.reduce( + ( + xy[:, 1] < img_shape[2], + xy[:, 1] > 0, + xy[:, 2] < img_shape[1], + xy[:, 2] > 0, + ) + ) + xy = xy[inside] + if xy.size: + has_data = True + joints[n] = xy + + if has_data: + data = { + "image": filename, + "size": np.asarray(img_shape), + "joints": joints, + } + train_data.append(data) + + return train_data def create_multianimaltraining_dataset( - config, + config: str | Path | ProjectConfig | dict, num_shuffles=1, Shuffles=None, windows2linux=False, net_type=None, + detector_type=None, numdigits=2, + crop_size=(400, 400), + crop_sampling="hybrid", paf_graph=None, + trainIndices=None, + testIndices=None, + n_edges_threshold=105, + paf_graph_degree=6, + userfeedback: bool = True, + weight_init: WeightInitialization | None = None, + 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_video' 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. - Imporant differences to standard: + Important differences to standard: - stores coordinates with numdigits as many digits - - creates - Parameter - ---------- - config : string - Full path of the config.yaml file as a string. - - num_shuffles : int, optional - Number of shuffles of training dataset to create, i.e. [1,2,3] for num_shuffles=3. Default is set to 1. - Shuffles: list of shuffles. - Alternatively the user can also give a list of shuffles (integers!). - - windows2linux: bool. - The annotation files contain path formated according to your operating system. If you label on windows - but train & evaluate on a unix system (e.g. ubunt, colab, Mac) set this variable to True to convert the paths. + Args: + config (str | Path | ProjectConfig | dict): Full path of the config.yaml file. + Alternatively, a ProjectConfig object or a dictionary can be passed. + num_shuffles (int, optional): Number of shuffles of training dataset to create, + i.e. [1,2,3] for num_shuffles=3. Defaults to 1. + Shuffles (list of shuffles): Alternatively the user can also give a list of + shuffles (integers!). + net_type (string): Type of networks. The options available depend on which + engine is used. See Lauer et al. 2021 + https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1 + Currently supported options are: + TensorFlow + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` + for a complete list) + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``dekr_w18`` + * ``dekr_w32`` + * ``dekr_w48`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` + * ``top_down_hrnet_w18`` + * ``top_down_hrnet_w32`` + * ``top_down_hrnet_w48`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` + detector_type (string, optional): Only for the PyTorch engine. When passing + creating shuffles for top-down models, you can specify which detector you + want. If the detector_type is None, the ```ssdlite``` will be used. The list + of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported + options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` + numdigits (int, optional): Number of decimal digits for stored coordinates. + crop_size (tuple of int, optional): Only for the TensorFlow engine. Dimensions + (width, height) of the crops for data augmentation. Defaults to 400x400. + crop_sampling (str, optional): Only for the TensorFlow engine. Crop centers + sampling method. Must be either: "uniform" (randomly over the image), + "keypoints" (randomly over the annotated keypoints), "density" (weighing + preferentially dense regions of keypoints), or "hybrid" (alternating + randomly between "uniform" and "density"). Defaults to "hybrid". + paf_graph (list of lists, or "config", optional): Only for the TensorFlow + engine. If not None, overwrite the default complete graph. This is useful + for advanced users who already know a good graph, or simply want to use a + specific one. Note that, in that case, the data-driven selection procedure + upon model evaluation will be skipped. "config" will use the skeleton + defined in the config file. Defaults to None. + trainIndices (list of lists, optional): List of one or multiple lists containing + train indexes. A list containing two lists of training indexes will produce + two splits. Defaults to None. + testIndices (list of lists, optional): List of one or multiple lists containing + test indexes. Defaults to None. + n_edges_threshold (int, optional): Only for the TensorFlow engine. Number of + edges above which the graph is automatically pruned. Defaults to 105. + paf_graph_degree (int, optional): Only for the TensorFlow engine. Degree of + paf_graph when automatically pruning it (before training). Defaults to 6. + userfeedback (bool, optional): If ``False``, all requested train/test splits + are created (no matter if they already exist). If you want to assure that + previous splits etc. are not overwritten, set this to ``True`` and you will + be asked for each split. Defaults to True. + weight_init (WeightInitialisation, optional): PyTorch engine only. Specify how + model weights should be initialized. The default mode uses transfer + learning from ImageNet weights. Defaults to None. + engine (Engine, optional): Whether to create a pose config for a Tensorflow or + PyTorch model. Defaults to the value specified in the project configuration + file. If no engine is specified for the project, defaults to + ``deeplabcut.compat.DEFAULT_ENGINE``. + ctd_conditions (int | str | Path | tuple[int, str] | tuple[int, int], optional): + If using a conditional-top-down (CTD) net_type, this argument needs to be + specified. 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. + Defaults to None. + + Examples: + + deeplabcut.create_multianimaltraining_dataset( + "/analysis/project/reaching-task/config.yaml", + num_shuffles=1, + ) - net_type: string - Type of networks. Currently resnet_50, resnet_101, and resnet_152 are supported (not the MobileNets!) + deeplabcut.create_multianimaltraining_dataset( + "/analysis/project/reaching-task/config.yaml", + Shuffles=[0, 1, 2], + trainIndices=[trainInd1, trainInd2, trainInd3], + testIndices=[testInd1, testInd2, testInd3], + ) - numdigits: int, optional + Windows: - paf_graph: list of lists, optional (default=None) - If not None, overwrite the default complete graph. This is useful for advanced users who - already know a good graph, or simply want to use a specific one. Note that, in that case, - the data-driven selection procedure upon model evaluation will be skipped. + deeplabcut.create_multianimaltraining_dataset( + r"C:\\Users\\Ulf\\looming-task\\config.yaml", + Shuffles=[3, 17, 5], + ) + """ + if windows2linux: + warnings.warn( + "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", + FutureWarning, + stacklevel=2, + ) - Example - -------- - >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml',num_shuffles=1) + if len(crop_size) != 2 or not all(isinstance(v, int) for v in crop_size): + raise ValueError("Crop size must be a tuple of two integers (width, height).") - Windows: - >>> deeplabcut.create_multianimaltraining_dataset(r'C:\\Users\\Ulf\\looming-task\\config.yaml',Shuffles=[3,17,5]) - -------- - """ - from skimage import io + if crop_sampling not in ("uniform", "keypoints", "density", "hybrid"): + raise ValueError( + f"Invalid sampling {crop_sampling}. Must be either 'uniform', 'keypoints', 'density', or 'hybrid." + ) # Loading metadata from config file: - cfg = auxiliaryfunctions.read_config(config) + cfg = ProjectConfig.from_any(config, repair_path=True) scorer = cfg["scorer"] project_path = cfg["project_path"] # Create path for training sets & store data there - trainingsetfolder = auxiliaryfunctions.GetTrainingSetFolder(cfg) + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) full_training_path = Path(project_path, trainingsetfolder) - auxiliaryfunctions.attempttomakefolder(full_training_path, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(full_training_path, recursive=True) - Data = trainingsetmanipulation.merge_annotateddatasets( - cfg, full_training_path, windows2linux - ) + # Create the trainset metadata file, if it doesn't yet exist + if not metadata.TrainingDatasetMetadata.path(cfg).exists(): + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + trainset_metadata.save() + + Data = merge_annotateddatasets(cfg, full_training_path) if Data is None: return - Data = Data[scorer] # extract labeled data - # actualbpts=set(Data.columns.get_level_values(0)) + Data = Data[scorer] - def strip_cropped_image_name(path): - # utility function to split different crops from same image into either train or test! - filename = os.path.split(path)[1] - return filename.split("c")[0] if cfg["croppedtraining"] else filename + if net_type is None: # loading & linking pretrained models + net_type = cfg.get("default_net_type", "dlcrnet_ms5") - img_names = Data.index.map(strip_cropped_image_name).unique() + # load the engine to use to create the shuffle + if engine is None: + engine = compat.get_project_engine(cfg) - # loading & linking pretrained models - # CURRENTLY ONLY ResNet supported! - if net_type is None: # loading & linking pretrained models - net_type = cfg.get("default_net_type", "resnet_50") - elif not any(net in net_type for net in ("resnet", "eff", "dlc")): - raise ValueError(f"Unsupported network {net_type}.") + 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 - if net_type == "dlcrnet_ms5": - net_type = "resnet_50" + ### dlcnet_ms5: backbone resnet50 + multi-fusion & multi-stage module + ### dlcr101_ms5/dlcr152_ms5: backbone resnet101/152 + multi-fusion & multi-stage module + if all(net in net_type for net in ("dlcr", "_ms5")) and engine != Engine.PYTORCH: + num_layers = re.findall("dlcr([0-9]*)", net_type)[0] + if num_layers == "": + num_layers = 50 + net_type = f"resnet_{num_layers}" multi_stage = True - # multianimal case: dataset_type = "multi-animal-imgaug" ( individuals, @@ -117,222 +323,157 @@ def strip_cropped_image_name(path): ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) if paf_graph is None: # Automatically form a complete PAF graph - partaffinityfield_graph = [ - list(edge) for edge in combinations(range(len(multianimalbodyparts)), 2) - ] + n_bpts = len(multianimalbodyparts) + 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; + # see Suppl. Fig S9c in Lauer et al., 2022. + if n_edges_orig >= n_edges_threshold: + partaffinityfield_graph = auxfun_multianimal.prune_paf_graph( + partaffinityfield_graph, + average_degree=paf_graph_degree, + ) else: + if paf_graph == "config": + # 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 + ] + 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 - ] + 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] auxfun_multianimal.validate_paf_graph(cfg, partaffinityfield_graph) - print("Utilizing the following graph:", partaffinityfield_graph) - num_limbs = len(partaffinityfield_graph) - partaffinityfield_predict = True + # Disable the prediction of PAFs if the graph is empty + partaffinityfield_predict = bool(partaffinityfield_graph) # Loading the encoder (if necessary downloading from TF) dlcparent_path = auxiliaryfunctions.get_deeplabcut_path() - defaultconfigfile = os.path.join(dlcparent_path, "pose_cfg.yaml") - model_path, num_shuffles = auxfun_models.Check4weights( - net_type, Path(dlcparent_path), num_shuffles - ) + defaultconfigfile = dlcparent_path / "pose_cfg.yaml" - if Shuffles == None: - Shuffles = range(1, num_shuffles + 1, 1) + if engine == Engine.PYTORCH: + model_path = dlcparent_path else: - Shuffles = [i for i in Shuffles if isinstance(i, int)] - - TrainingFraction = cfg["TrainingFraction"] - for shuffle in Shuffles: # Creating shuffles starting from 1 - for trainFraction in TrainingFraction: - train_inds_temp, test_inds_temp = trainingsetmanipulation.SplitTrials( - range(len(img_names)), trainFraction - ) - # Map back to the original indices. - temp = [name for i, name in enumerate(img_names) if i in test_inds_temp] - mask = Data.index.str.contains("|".join(temp)) - testIndices = np.flatnonzero(mask) - trainIndices = np.flatnonzero(~mask) - - #################################################### - # Generating data structure with labeled information & frame metadata (for deep cut) - #################################################### - - # Make training file! - data = [] - print( - "Creating training data for: Shuffle:", - shuffle, - "TrainFraction: ", - trainFraction, - ) - print("This can take some time...") - for jj in tqdm(trainIndices): - jointsannotated = False - H = {} - # load image to get dimensions: - filename = Data.index[jj] - im = io.imread(os.path.join(cfg["project_path"], filename)) - H["image"] = filename - - try: - H["size"] = np.array( - [np.shape(im)[2], np.shape(im)[0], np.shape(im)[1]] - ) - except: - # print "Grayscale!" - H["size"] = np.array([1, np.shape(im)[0], np.shape(im)[1]]) - - Joints = {} - for prfxindex, prefix in enumerate(individuals): - joints = ( - np.zeros((len(uniquebodyparts) + len(multianimalbodyparts), 3)) - * np.nan - ) - if prefix != "single": # first ones are multianimalparts! - indexjoints = 0 - for bpindex, bodypart in enumerate(multianimalbodyparts): - socialbdpt = bodypart # prefix+bodypart #build names! - # if socialbdpt in actualbpts: - try: - x, y = ( - Data[prefix][socialbdpt]["x"][jj], - Data[prefix][socialbdpt]["y"][jj], - ) - joints[indexjoints, 0] = int(bpindex) - joints[indexjoints, 1] = round(x, numdigits) - joints[indexjoints, 2] = round(y, numdigits) - indexjoints += 1 - except: - pass - else: - indexjoints = len(multianimalbodyparts) - for bpindex, bodypart in enumerate(uniquebodyparts): - socialbdpt = bodypart # prefix+bodypart #build names! - # if socialbdpt in actualbpts: - try: - x, y = ( - Data[prefix][socialbdpt]["x"][jj], - Data[prefix][socialbdpt]["y"][jj], - ) - joints[indexjoints, 0] = len( - multianimalbodyparts - ) + int(bpindex) - joints[indexjoints, 1] = round(x, 2) - joints[indexjoints, 2] = round(y, 2) - indexjoints += 1 - except: - pass - - # Drop missing body parts - joints = joints[~np.isnan(joints).any(axis=1)] - # Drop points lying outside the image - inside = np.logical_and.reduce( - ( - joints[:, 1] < im.shape[1], - joints[:, 1] > 0, - joints[:, 2] < im.shape[0], - joints[:, 2] > 0, - ) - ) - joints = joints[inside] + model_path = auxfun_models.check_for_weights(net_type, dlcparent_path) - if np.size(joints) > 0: # exclude images without labels - jointsannotated = True + Shuffles = validate_shuffles(cfg, Shuffles, num_shuffles, userfeedback) - Joints[prfxindex] = joints # np.array(joints, dtype=int) + # print(trainIndices,testIndices, Shuffles, augmenter_type,net_type) + if trainIndices is None and testIndices is None: + splits = [] + for shuffle in Shuffles: # Creating shuffles starting from 1 + for train_frac in cfg["TrainingFraction"]: + train_inds, test_inds = SplitTrials(range(len(Data)), train_frac) + 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.") + splits = [] + 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, + # 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))) + + top_down = False + if engine == Engine.PYTORCH and net_type.startswith("top_down_"): + top_down = True + net_type = net_type[len("top_down_") :] + + for trainFraction, shuffle, (trainIndices, testIndices) in splits: + #################################################### + # Generating data structure with labeled information & frame metadata (for deep cut) + #################################################### + print( + "Creating training data for: Shuffle:", + shuffle, + "TrainFraction: ", + trainFraction, + ) - H["joints"] = Joints - if jointsannotated: # exclude images without labels - data.append(H) + # Make training file! + data = format_multianimal_training_data( + Data, + trainIndices, + cfg["project_path"], + numdigits, + ) - if len(trainIndices) > 0: - ( - datafilename, - metadatafilename, - ) = auxiliaryfunctions.GetDataandMetaDataFilenames( - trainingsetfolder, trainFraction, shuffle, cfg - ) - ################################################################################ - # Saving metadata and data file (Pickle file) - ################################################################################ - auxiliaryfunctions.SaveMetadata( - os.path.join(project_path, metadatafilename), - data, - trainIndices, - testIndices, - trainFraction, - ) + if len(trainIndices) > 0: + ( + datafilename, + metadatafilename, + ) = auxiliaryfunctions.get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg) + ################################################################################ + # Saving metadata and data file (Pickle file) + ################################################################################ + auxiliaryfunctions.save_metadata( + Path(project_path) / metadatafilename, + data, + trainIndices, + testIndices, + trainFraction, + ) + metadata.update_metadata( + cfg=cfg, + train_fraction=trainFraction, + shuffle=shuffle, + engine=engine, + train_indices=trainIndices, + test_indices=testIndices, + overwrite=not userfeedback, + ) - datafilename = datafilename.split(".mat")[0] + ".pickle" - import pickle + datafilename = datafilename.with_suffix(".pickle") + import pickle - with open(os.path.join(project_path, datafilename), "wb") as f: - # Pickle the 'labeled-data' dictionary using the highest protocol available. - pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) + with (Path(project_path) / datafilename).open("wb") as f: + # Pickle the 'labeled-data' dictionary using the highest protocol available. + pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) - ################################################################################ - # Creating file structure for training & - # Test files as well as pose_yaml files (containing training and testing information) - ################################################################################# + ################################################################################ + # Creating file structure for training & + # Test files as well as pose_yaml files (containing training and testing information) + ################################################################################# - modelfoldername = auxiliaryfunctions.GetModelFolder( - trainFraction, shuffle, cfg - ) - auxiliaryfunctions.attempttomakefolder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempttomakefolder( - str(Path(config).parents[0] / modelfoldername / "train") - ) - auxiliaryfunctions.attempttomakefolder( - str(Path(config).parents[0] / modelfoldername / "test") - ) + modelfoldername = auxiliaryfunctions.get_model_folder( + trainFraction, + shuffle, + cfg, + engine=engine, + ) + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername / "train") + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername / "test") - path_train_config = str( - os.path.join( - cfg["project_path"], - Path(modelfoldername), - "train", - "pose_cfg.yaml", - ) - ) - path_test_config = str( - os.path.join( - cfg["project_path"], - Path(modelfoldername), - "test", - "pose_cfg.yaml", - ) - ) - path_inference_config = str( - os.path.join( - cfg["project_path"], - Path(modelfoldername), - "test", - "inference_cfg.yaml", - ) - ) + path_train_config = str(Path(cfg["project_path"]) / modelfoldername / "train" / "pose_cfg.yaml") + path_test_config = str(Path(cfg["project_path"]) / modelfoldername / "test" / "pose_cfg.yaml") + path_inference_config = str(Path(cfg["project_path"]) / modelfoldername / "test" / "inference_cfg.yaml") + if engine == Engine.TF: jointnames = [str(bpt) for bpt in multianimalbodyparts] jointnames.extend([str(bpt) for bpt in uniquebodyparts]) items2change = { "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": model_path, + "init_weights": str(model_path), "project_path": str(cfg["project_path"]), "net_type": net_type, "multi_stage": multi_stage, @@ -348,14 +489,16 @@ def strip_cropped_image_name(path): "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, } - defaultconfigfile = os.path.join(dlcparent_path, "pose_cfg.yaml") - trainingdata = trainingsetmanipulation.MakeTrain_pose_yaml( - items2change, path_train_config, defaultconfigfile + trainingdata = MakeTrain_pose_yaml( + items2change, + path_train_config, + defaultconfigfile, + save=(engine == Engine.TF), ) keys2save = [ "dataset", @@ -377,32 +520,166 @@ def strip_cropped_image_name(path): "num_idchannel", ] - trainingsetmanipulation.MakeTest_pose_yaml( + MakeTest_pose_yaml( trainingdata, keys2save, path_test_config, nmsradius=5.0, minconfidence=0.01, + sigma=1, + locref_smooth=False, ) # setting important def. values for inference - - # Setting inference cfg file: - defaultinference_configfile = os.path.join( - dlcparent_path, "inference_cfg.yaml" + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_pytorch_pose_config, + make_pytorch_test_config, ) - items2change = { - "minimalnumberofconnections": int( - len(cfg["multianimalbodyparts"]) / 2 - ), - "topktoretain": len(cfg["individuals"]) - + 1 * (len(cfg["uniquebodyparts"]) > 0), - "withid": cfg.get("identity", False), - } - trainingsetmanipulation.MakeInference_yaml( - items2change, path_inference_config, defaultinference_configfile + from deeplabcut.pose_estimation_pytorch.modelzoo.config import ( + make_super_animal_finetune_config, ) - print( - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!" - ) - else: - pass + # backwards compatibility with version 2.X + if net_type == "dlcrnet_ms5": + net_type = "dlcrnet_stride16_ms5" + + config_path = Path(path_train_config).with_name(engine.pose_cfg_name) + if weight_init is not None and weight_init.with_decoder: + pytorch_cfg = make_super_animal_finetune_config( + project_config=cfg, + pose_config_path=config_path, + model_name=net_type, + detector_name=detector_type, + weight_init=weight_init, + save=True, + ) + else: + pytorch_cfg = make_pytorch_pose_config( + project_config=cfg, + pose_config_path=config_path, + net_type=net_type, + top_down=top_down, + detector_type=detector_type, + weight_init=weight_init, + save=True, + ctd_conditions=ctd_conditions, + ) + + make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) + + # Setting inference cfg file: + default_inf_path = dlcparent_path / "inference_cfg.yaml" + inf_updates = dict( + minimalnumberofconnections=int(len(cfg["multianimalbodyparts"]) / 2), + topktoretain=len(cfg["individuals"]), + withid=cfg.get("identity", False), + ) + MakeInference_yaml(inf_updates, path_inference_config, default_inf_path) + + print( + "The training dataset is successfully created. Use the function " + "'train_network' to start training. Happy training!" + ) + else: + pass + + +# TODO @deruyter92 2026-06-05: This function seems to be unused dead code. Let's remove it. +def convert_cropped_to_standard_dataset( + config_path, + recreate_datasets=True, + delete_crops=True, + back_up=True, +): + import pickle + import shutil + + import pandas as pd + + from deeplabcut.utils import read_plainconfig, write_config + + cfg = auxiliaryfunctions.read_config(config_path) + 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...") + return + + project_path = cfg["project_path"] + + if back_up: + print("Backing up project...") + shutil.copytree(project_path, project_path + "_bak", symlinks=True) + + if delete_crops: + print("Deleting crops...") + data_path = Path(project_path) / "labeled-data" + for video in cfg["video_sets"]: + filename = Path(video).stem + if "_cropped" in video: # One can never be too safe... + shutil.rmtree(data_path / filename, ignore_errors=True) + + cfg["video_sets"] = videos_orig + write_config(config_path, cfg) + + if not recreate_datasets: + return + + datasets_folder = Path(project_path) / auxiliaryfunctions.get_training_set_folder(cfg) + df_old = pd.read_hdf( + datasets_folder / ("CollectedData_" + cfg["scorer"] + ".h5"), + ) + + def strip_cropped_image_name(path): + path_obj = Path(path) + head = str(path_obj.parent).replace("_cropped", "") + filename = path_obj.name + file, ext = filename.split(".") + file = file.split("c")[0] + return str(Path(head) / (file + "." + ext)) + + 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 = [] + test_idx = [] + pickle_files = [] + for p in datasets_folder.iterdir(): + if p.name.endswith("pickle"): + pickle_file = p + pickle_files.append(pickle_file) + if p.name.startswith("Docu"): + with p.open("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_idx.append(train_inds) + test_idx.append(test_inds) + + # Search a pose_config.yaml file to parse missing information + pose_config_path = "" + for dirpath, _, filenames in os.walk(Path(project_path) / "dlc-models"): + for file in filenames: + if file.endswith("pose_cfg.yaml"): + pose_config_path = str(Path(dirpath) / file) + break + pose_cfg = read_plainconfig(pose_config_path) + net_type = pose_cfg["net_type"] + if net_type == "resnet_50" and pose_cfg.get("multi_stage", False): + net_type = "dlcrnet_ms5" + + # Clean the training-datasets folder prior to recreating the data pickles + shuffle_inds = set() + for file in pickle_files: + file.unlink() + shuffle_inds.add(int(re.findall(r"shuffle(\d+)", str(file))[0])) + create_multianimaltraining_dataset( + config_path, + trainIndices=train_idx, + testIndices=test_idx, + Shuffles=sorted(shuffle_inds), + net_type=net_type, + paf_graph=pose_cfg["partaffinityfield_graph"], + crop_size=pose_cfg.get("crop_size", [400, 400]), + crop_sampling=pose_cfg.get("crop_sampling", "hybrid"), + ) diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 4a3fd1707f..a9bc73398b 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -1,53 +1,51 @@ -""" -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 logging -import os -import os.path -import shutil +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations -from functools import lru_cache +import logging +import math +import warnings +from functools import cache from pathlib import Path import numpy as np import pandas as pd import yaml -from skimage import io +from PIL import Image -from deeplabcut.pose_estimation_tensorflow import training +import deeplabcut.compat as compat +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.config import ProjectConfig, read_config, write_config +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 -def comparevideolistsanddatafolders(config): - """ - 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. +def comparevideolistsanddatafolders(config: str | Path): + """Auxiliary function that compares the folders in labeled-data and the ones listed + under video_sets (in the config file). + Args: + config (string): String containing the full path of the config file in the project. """ - cfg = auxiliaryfunctions.read_config(config) + cfg = 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 = [f.name for f in (Path(config).parent / "labeled-data").iterdir() if "_labeled" not in f.name] print("Config file contains:", len(video_names)) print("Labeled-data contains:", len(alldatafolders)) @@ -61,32 +59,31 @@ def comparevideolistsanddatafolders(config): print(vn, " is missing in config file!") -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. +def adddatasetstovideolistandviceversa(config: str | Path): + """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: 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! - Parameter - ---------- - config : string - String containing the full path of the config file in the project. + Args: + config (string): String containing the full path of the config file in the project. """ - cfg = auxiliaryfunctions.read_config(config) + cfg = read_config(config) videos = cfg["video_sets"] video_names = [Path(i).stem for i in videos] + labeled_data_dir = Path(config).parent / "labeled-data" alldatafolders = [ - fn - for fn in os.listdir(Path(config).parent / "labeled-data") - if "_labeled" not in fn and not fn.startswith(".") + f.name for f in labeled_data_dir.iterdir() if "_labeled" not in f.name and not f.name.startswith(".") ] print("Config file contains:", len(video_names)) @@ -110,122 +107,113 @@ def adddatasetstovideolistandviceversa(config): print(vn, " is missing in config file >> adding it!") # Find the corresponding video file found = False - for file in os.listdir(os.path.join(cfg["project_path"], "videos")): - if os.path.splitext(file)[0] == vn: + for file in (Path(cfg["project_path"]) / "videos").iterdir(): + if file.stem == vn: found = True break if found: - video_path = os.path.join(cfg["project_path"], "videos", file) + video_path = str(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) -def dropduplicatesinannotatinfiles(config): - """ - - 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. +def dropduplicatesinannotatinfiles(config: str | Path): + """Drop duplicate entries (of images) in annotation files (this should no longer + happen, but might be useful). + Args: + config (string): String containing the full path of the config file in the project. """ - cfg = auxiliaryfunctions.read_config(config) + cfg = read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] for folder in folders: try: - fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") + fn = folder / ("CollectedData_" + cfg["scorer"] + ".h5") DC = pd.read_hdf(fn) numimages = len(DC.index) DC = DC[~DC.index.duplicated(keep="first")] 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(folder / ("CollectedData_" + cfg["scorer"] + ".csv")) except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") -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. - - Parameter - ---------- - config : string - String containing the full path of the config file in the project. +def dropannotationfileentriesduetodeletedimages(config: str | Path): + """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. + Args: + config (string): String containing the full path of the config file in the project. """ - cfg = auxiliaryfunctions.read_config(config) + cfg = read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] for folder in folders: - fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") - DC = pd.read_hdf(fn) + fn = folder / ("CollectedData_" + cfg["scorer"] + ".h5") + try: + DC = pd.read_hdf(fn) + except FileNotFoundError: + print("Attention:", folder, "does not appear to have labeled data!") + continue dropped = False for imagename in DC.index: - if os.path.isfile(os.path.join(cfg["project_path"], imagename)): + if Path(cfg["project_path"]).joinpath(*imagename).is_file(): pass else: 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") - ) + DC.to_csv(folder / ("CollectedData_" + cfg["scorer"] + ".csv")) -def dropimagesduetolackofannotation(config): +def dropimagesduetolackofannotation(config: str | Path): """ Drop images from corresponding folder for not annotated images: /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. + Args: + config (string): String containing the full path of the config file in the project. """ - cfg = auxiliaryfunctions.read_config(config) + cfg = read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] for folder in folders: - fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") - DC = pd.read_hdf(fn) - dropped = False - annotatedimages = [fn.split(os.sep)[-1] for fn in DC.index] - imagelist = [fns for fns in os.listdir(str(folder)) if ".png" in fns] + h5file = folder / ("CollectedData_" + cfg["scorer"] + ".h5") + try: + DC = pd.read_hdf(h5file) + except FileNotFoundError: + print("Attention:", folder, "does not appear to have labeled data!") + continue + conversioncode.guarantee_multiindex_rows(DC) + annotatedimages = [fn[-1] for fn in DC.index] + imagelist = [f.name for f in folder.iterdir() if ".png" in f.name] print("Annotated images: ", len(annotatedimages), " In folder:", len(imagelist)) for imagename in imagelist: if imagename in annotatedimages: pass else: - fullpath = os.path.join( - cfg["project_path"], "labeled-data", folder, imagename - ) - if os.path.isfile(fullpath): + fullpath = folder / imagename + if fullpath.is_file(): print("Deleting", fullpath) - os.remove(fullpath) + fullpath.unlink() - annotatedimages = [fn.split(os.sep)[-1] for fn in DC.index] - imagelist = [fns for fns in os.listdir(str(folder)) if ".png" in fns] + annotatedimages = [fn[-1] for fn in DC.index] + imagelist = [f.name for f in folder.iterdir() if ".png" in f.name] print( "PROCESSED:", folder, @@ -236,240 +224,91 @@ def dropimagesduetolackofannotation(config): ) -def cropimagesandlabels( - config, - numcrops=10, - size=(400, 400), - userfeedback=True, - cropdata=True, - excludealreadycropped=False, - updatevideoentries=True, -): - """ - Crop images into multiple random crops (defined by numcrops) of size dimensions. If cropdata=True then the - annotation data is loaded and labels for cropped images are inherited. - If false, then one can make crops for unlabeled folders. - - This can be helpul for large frames with multiple animals. Then a smaller set of equally sized images is created. - - Parameters - ---------- - config : string - String containing the full path of the config file in the project. - - numcrops: number of random crops (around random bodypart) - - size: height x width in pixels - - userfeedback: bool, optional - If this is set to false, then all requested train/test splits are created (no matter if they already exist). If you - want to assure that previous splits etc. are not overwritten, then set this to True and you will be asked for each split. - - cropdata: bool, default True: - If true creates corresponding annotation data (from ground truth) - - excludealreadycropped: bool, default False: - If true, ignore original videos whose frames are already cropped. - This is only useful after adding new videos post dataset creation, - as folders containing no new frames are otherwise automatically ignored. +def dropunlabeledframes(config: str | Path): + """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. - updatevideoentries, bool, default true - If true updates video_list entries to refer to cropped frames instead. This makes sense for subsequent processing. - - Example - -------- - for labeling the frames - >>> deeplabcut.cropimagesandlabels('/analysis/project/reaching-task/config.yaml') - - -------- + Args: + config (string): String containing the full path of the config file in the project. """ - from tqdm import trange - - indexlength = int(np.ceil(np.log10(numcrops))) - project_path = os.path.dirname(config) - cfg = auxiliaryfunctions.read_config(config) - videos = cfg.get("video_sets_original") - if videos is None: - videos = cfg["video_sets"] - elif excludealreadycropped: - for video in list(videos): - _, ext = os.path.splitext(video) - s = video.replace(ext, f"_cropped{ext}") - if s in cfg["video_sets"]: - videos.pop(video) - if not videos: - return + cfg = read_config(config) + videos = cfg["video_sets"].keys() + video_names = [Path(i).stem for i in videos] + folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] - if ( - "video_sets_original" not in cfg.keys() and updatevideoentries - ): # this dict is kept for storing links to original full-sized videos - cfg["video_sets_original"] = {} - - for video in list(videos): - vidpath, vidname, videotype = _robust_path_split(video) - folder = os.path.join(project_path, "labeled-data", vidname) - if userfeedback: - print("Do you want to crop frames for folder: ", folder, "?") - askuser = input("(yes/no):") - else: - askuser = "y" - if askuser == "y" or askuser == "yes" or askuser == "Y" or askuser == "Yes": - new_vidname = vidname + "_cropped" - new_folder = os.path.join(project_path, "labeled-data", new_vidname) - auxiliaryfunctions.attempttomakefolder(new_folder) - - AnnotationData = [] - pd_index = [] - - fn = os.path.join(folder, f"CollectedData_{cfg['scorer']}.h5") - df = pd.read_hdf(fn) - data = df.values.reshape((df.shape[0], -1, 2)) - sep = "/" if "/" in df.index[0] else "\\" - if sep != os.path.sep: - df.index = df.index.str.replace(sep, os.path.sep) - video_new = sep.join((vidpath, new_vidname + videotype)) - if video_new in cfg["video_sets"]: - _, w, _, h = map(int, cfg["video_sets"][video_new]["crop"].split(",")) - temp_size = (h, w) - else: - temp_size = size - images = project_path + os.path.sep + df.index - # Avoid cropping already cropped images - cropped_images = auxiliaryfunctions.grab_files_in_folder(new_folder, "png") - cropped_names = set(map(lambda x: x.split("c")[0], cropped_images)) - imnames = [ - im for im in images.to_list() if Path(im).stem not in cropped_names - ] - if not imnames: - continue - ic = io.imread_collection(imnames) - for i in trange(len(ic)): - frame = ic[i] - h, w = np.shape(frame)[:2] - if temp_size[0] >= h or temp_size[1] >= w: - raise ValueError("Crop dimensions are larger than image size") - - imagename = os.path.relpath(ic.files[i], project_path) - ind = np.flatnonzero(df.index == imagename)[0] - cropindex = 0 - attempts = -1 - while cropindex < numcrops: - dd = np.array(data[ind].copy(), dtype=float) - y0, x0 = ( - np.random.randint(h - temp_size[0]), - np.random.randint(w - temp_size[1]), - ) - y1 = y0 + temp_size[0] - x1 = x0 + temp_size[1] - with np.errstate(invalid="ignore"): - within = np.all((dd >= [x0, y0]) & (dd < [x1, y1]), axis=1) - if cropdata: - dd[within] -= [x0, y0] - dd[~within] = np.nan - attempts += 1 - if within.any() or attempts > 10: - newimname = str( - Path(imagename).stem - + "c" - + str(cropindex).zfill(indexlength) - + ".png" - ) - cropppedimgname = os.path.join(new_folder, newimname) - io.imsave(cropppedimgname, frame[y0:y1, x0:x1]) - cropindex += 1 - pd_index.append( - os.path.join("labeled-data", new_vidname, newimname) - ) - AnnotationData.append(dd.flatten()) - - if cropdata: - df = pd.DataFrame(AnnotationData, index=pd_index, columns=df.columns) - fn_new = fn.replace(folder, new_folder) - try: - df_old = pd.read_hdf(fn_new) - df = pd.concat((df_old, df)) - except FileNotFoundError: - pass - df.to_hdf(fn_new, key="df_with_missing", mode="w") - df.to_csv(fn_new.replace(".h5", ".csv")) - - if updatevideoentries and cropdata: - # moving old entry to _original, dropping it from video_set and update crop parameters - video_orig = sep.join((vidpath, vidname + videotype)) - video_new = sep.join((vidpath, new_vidname + videotype)) - if video_orig not in cfg["video_sets_original"]: - cfg["video_sets_original"][video_orig] = cfg["video_sets"][ - video_orig - ] - cfg["video_sets"].pop(video_orig) - cfg["video_sets"][video_new] = { - "crop": ", ".join(map(str, [0, temp_size[1], 0, temp_size[0]])) - } - elif video_new not in cfg["video_sets"]: - cfg["video_sets"][video_new] = { - "crop": ", ".join(map(str, [0, temp_size[1], 0, temp_size[0]])) - } + for folder in folders: + h5file = folder / ("CollectedData_" + cfg["scorer"] + ".h5") + try: + DC = pd.read_hdf(h5file) + except FileNotFoundError: + print("Skipping ", folder, "...") + continue + before_len = len(DC.index) + DC = DC.dropna(how="all") # drop rows where all values are missing(NaN) + after_len = len(DC.index) + dropped = before_len - after_len + if dropped: + DC.to_hdf(h5file, key="df_with_missing", mode="w") + DC.to_csv(folder / ("CollectedData_" + cfg["scorer"] + ".csv")) - cfg["croppedtraining"] = True - auxiliaryfunctions.write_config(config, cfg) + print("Dropped ", dropped, "entries in ", folder) + + print("Done.") def check_labels( - config, - Labels=["+", ".", "x"], + config: str | Path, + Labels=None, scale=1, dpi=100, draw_skeleton=True, visualizeindividuals=True, ): - """ - Double check if the labels were at correct locations and stored in a proper file format.\n - This creates a new subdirectory for each video under the 'labeled-data' and all the frames are plotted with the labels.\n - Make sure that these labels are fine. + """Check the labeled frames. - Parameter - ---------- - config : string - Full path of the config.yaml file as a string. + Double check if the labels were at the correct locations and stored in the proper + file format. - Labels: List of at least 3 matplotlib markers. The first one will be used to indicate the human ground truth location (Default: +) + This creates a new subdirectory for each video under the 'labeled-data' and all the + frames are plotted with the labels. - scale : float, default =1 - Change the relative size of the output images. - - dpi : int, optional - Output resolution. 100 dpi by default. - - draw_skeleton: bool, default True. - Plot skeleton overlaid over body parts. - - visualizeindividuals: bool, default True: - For a multianimal project the different individuals have different colors (and all bodyparts the same). - If False, the colors change over bodyparts rather than individuals. + Make sure that these labels are fine. - Example - -------- - for labeling the frames - >>> deeplabcut.check_labels('/analysis/project/reaching-task/config.yaml') - -------- + Args: + config (string): Full path of the config.yaml file as a string. + Labels (list, optional): List of at least 3 matplotlib markers. The first one + will be used to indicate the human ground truth location. Defaults to '+'. + scale (float, optional): Change the relative size of the output images. + Defaults to 1. + dpi (int, optional): Output resolution in dpi. Defaults to 100. + draw_skeleton (bool, optional): Plot skeleton overlaid over body parts. + Defaults to True. + visualizeindividuals (bool, optional): For a multianimal project, if True, the + different individuals have different colors (and all bodyparts the same). + If False, the colors change over bodyparts rather than individuals. + Defaults to True. + + Returns: + None + + Examples: + deeplabcut.check_labels("/analysis/project/reaching-task/config.yaml") """ - from deeplabcut.utils import visualization - cfg = auxiliaryfunctions.read_config(config) + if Labels is None: + Labels = ["+", ".", "x"] + cfg = read_config(config) videos = cfg["video_sets"].keys() - video_names = [_robust_path_split(video)[1] for video in videos] + video_names = [Path(video).stem 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"]) + folders = [Path(cfg["project_path"]) / "labeled-data" / Path(i) for i in video_names] + 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") - ) + DataCombined = pd.read_hdf(folder / ("CollectedData_" + cfg["scorer"] + ".h5")) + conversioncode.guarantee_multiindex_rows(DataCombined) if cfg.get("multianimalproject", False): color_by = "individual" if visualizeindividuals else "bodypart" else: # for single animal projects @@ -488,19 +327,17 @@ 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): - """ Auxiliary function for creating matfile.""" + """Auxiliary function for creating matfile.""" outer = np.array([[None]], dtype=object) outer[0, 0] = np.array(joints, dtype="int64") return outer -def ParseYaml(configfile): +def ParseYaml(configfile: str | Path): raw = open(configfile).read() docs = [] for raw_doc in raw.split("\n---"): @@ -512,25 +349,37 @@ def ParseYaml(configfile): def MakeTrain_pose_yaml( - itemstochange, saveasconfigfile, defaultconfigfile, items2drop={} + itemstochange, + saveasconfigfile, + defaultconfigfile, + items2drop: dict | None = None, + save: bool = True, ): + if items2drop is None: + items2drop = {} + docs = ParseYaml(defaultconfigfile) for key in items2drop.keys(): - # print(key, "dropping?") if key in docs[0].keys(): docs[0].pop(key) for key in itemstochange.keys(): docs[0][key] = itemstochange[key] - with open(saveasconfigfile, "w") as f: - yaml.dump(docs[0], f) + if save: + write_config(saveasconfigfile, docs[0]) return docs[0] def MakeTest_pose_yaml( - dictionary, keys2save, saveasfile, nmsradius=None, minconfidence=None + dictionary, + keys2save, + saveasfile, + nmsradius=None, + minconfidence=None, + sigma=None, + locref_smooth=None, ): dict_test = {} for key in keys2save: @@ -541,10 +390,13 @@ def MakeTest_pose_yaml( dict_test["nmsradius"] = nmsradius if minconfidence is not None: dict_test["minconfidence"] = minconfidence + if sigma is not None: + dict_test["sigma"] = sigma + if locref_smooth is not None: + dict_test["locref_smooth"] = locref_smooth dict_test["scoremap_dir"] = "test" - with open(saveasfile, "w") as f: - yaml.dump(dict_test, f) + write_config(saveasfile, dict_test) def MakeInference_yaml(itemstochange, saveasconfigfile, defaultconfigfile): @@ -552,54 +404,113 @@ def MakeInference_yaml(itemstochange, saveasconfigfile, defaultconfigfile): for key in itemstochange.keys(): docs[0][key] = itemstochange[key] - with open(saveasconfigfile, "w") as f: - yaml.dump(docs[0], f) + write_config(saveasconfigfile, docs[0]) return docs[0] -def _robust_path_split(path): - sep = "\\" if "\\" in path else "/" - splits = path.rsplit(sep, 1) - if len(splits) == 1: - parent = '.' - file = splits[0] - elif len(splits) == 2: - parent, file = splits - else: - raise('Unknown filepath split for path {}'.format(path)) - filename, ext = os.path.splitext(file) - return parent, filename, ext +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 + duplicate video name (e.g. if a video is listed twice, once with the path + ``/data/video-1.mov`` and once with the path ``/my-dlc-project/videos/video-1.mov``, + then ``video-1`` will only be returned once). The order of videos listed is + preserved. + + This prevents the same labeled-data to be added multiple times when merging + annotated datasets. + Prints a warning for each filename with duplicate video paths. -def merge_annotateddatasets(cfg, trainingsetfolder_full, windows2linux): + Args: + videos: the videos listed in the project's config.yaml file + + Returns: + the filenames of videos listed in the project's config.yaml file, with duplicate + entries removed """ - Merges all the h5 files for all labeled-datasets (from individual videos). + filenames = [] + filename_to_videos = {} + for video in videos: + filename = Path(video).stem + videos_with_filename = filename_to_videos.get(filename, []) + if len(videos_with_filename) == 0: + filenames.append(filename) + + videos_with_filename.append(video) + filename_to_videos[filename] = videos_with_filename + + for filename, videos in filename_to_videos.items(): + if len(videos) > 1: + video_str = "\n * " + "\n * ".join(videos) + logging.warning( + f"Found multiple videos with the same filename (``{filename}``). To " + f"avoid issues, please edit your project's `config.yaml` file to have " + f"each video added only once.\nDuplicate entries: {video_str}" + ) + + return filenames + + +def drop_likelihood_columns(df: pd.DataFrame) -> pd.DataFrame: + """Drop any columns whose coord level is named 'likelihood'. + + This sanitizes annotation DataFrames coming from h5/csv files before they are + used for training dataset generation. + + # NOTE @C-Achard 2026-05-18: This is used in several places as a guard + Most call sites using this should instead go through a canonical, validated project loading function + AND THEN do any custom local processing they require. The current design is hard to maintain and error prone, + and lacks a clearly documented, centralized project I/O interface. + """ + if not isinstance(df.columns, pd.MultiIndex): + return df + + coord_level = "coords" if "coords" in df.columns.names else df.columns.names[-1] + coord_values = df.columns.get_level_values(coord_level) + + likelihood_mask = coord_values == "likelihood" + if likelihood_mask.any(): + logging.warning("Detected likelihood columns in annotation data; dropping them.", stacklevel=2) + df = df.drop(columns=df.columns[likelihood_mask]) + + return df + + +def merge_annotateddatasets(cfg, trainingsetfolder_full): + """Merges all the h5 files for all labeled-datasets (from individual videos). 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")) + data_path = Path(cfg["project_path"]) / "labeled-data" videos = cfg["video_sets"].keys() - for video in videos: - _, filename, _ = _robust_path_split(video) - file_path = os.path.join( - data_path / filename, f'CollectedData_{cfg["scorer"]}.h5' - ) + video_filenames = parse_video_filenames(videos) + for filename in video_filenames: + file_path = data_path / filename / f"CollectedData_{cfg['scorer']}.h5" try: data = pd.read_hdf(file_path) + 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)" + ) + continue AnnotationData.append(data) except FileNotFoundError: - print( - file_path, - " not found (perhaps not annotated). If training on cropped data, " - "make sure to call `cropimagesandlabels` prior to creating the dataset.", - ) + print(file_path, " not found (perhaps not annotated).") 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): @@ -619,39 +530,35 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full, windows2linux): bodyparts = multianimalbodyparts + uniquebodyparts else: bodyparts = cfg["bodyparts"] - AnnotationData = AnnotationData.reindex( - bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts") - ) - - # Let's check if the code is *not* run on windows (Source: #https://stackoverflow.com/questions/1325581/how-do-i-check-if-im-running-on-windows-in-python) - # but the paths are in windows format... - windowspath = "\\" in AnnotationData.index[0] - if os.name != "nt" and windowspath and not windows2linux: - print( - "It appears that the images were labeled on a Windows system, but you are currently trying to create a training set on a Unix system. \n In this case the paths should be converted. Do you want to proceed with the conversion?" + AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts")) + # Filter out any stray likelihood columns that may have been concatenated in + # see napari-deeplabcut #204 and DeepLabCut #3319 + AnnotationData = drop_likelihood_columns(AnnotationData) + + if AnnotationData.empty: + logging.warning( + "The annotated dataframe is empty after reindexing using config. " + "Hint: are bodyparts correctly listed in the configuration?" ) - askuser = input("yes/no") - else: - askuser = "no" - - filename = os.path.join(trainingsetfolder_full, f'CollectedData_{cfg["scorer"]}') - if ( - windows2linux or askuser == "yes" or askuser == "y" or askuser == "Ja" - ): # convert windows path in pandas array \\ to unix / ! - AnnotationData = conversioncode.convertpaths_to_unixstyle( - AnnotationData, filename - ) - print("Annotation data converted to unix format...") - else: # store as is - AnnotationData.to_hdf(filename + ".h5", key="df_with_missing", mode="w") - AnnotationData.to_csv(filename + ".csv") # human readable. + filename = trainingsetfolder_full / f"CollectedData_{cfg['scorer']}" + AnnotationData.to_hdf(str(filename) + ".h5", key="df_with_missing", mode="w") + AnnotationData.to_csv(str(filename) + ".csv") # human readable. return AnnotationData -def SplitTrials(trialindex, trainFraction=0.8): - """ 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). """ +def SplitTrials( + trialindex, + 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. + """ if trainFraction > 1 or trainFraction < 0: print( "The training fraction should be a two digit number between 0 and 1; i.e. 0.95. Please change accordingly." @@ -664,94 +571,135 @@ def SplitTrials(trialindex, trainFraction=0.8): ) return ([], []) else: - trainsetsize = int(len(trialindex) * round(trainFraction, 2)) + index_len = len(trialindex) + train_fraction = round(trainFraction, 2) + train_size = index_len * train_fraction shuffle = np.random.permutation(trialindex) - testIndices = shuffle[trainsetsize:] - trainIndices = shuffle[:trainsetsize] - - return (trainIndices, testIndices) + test_indices = shuffle[int(train_size) :] + train_indices = shuffle[: int(train_size)] + if enforce_train_fraction and not train_size.is_integer(): + train_indices, test_indices = pad_train_test_indices( + train_indices, + test_indices, + train_fraction, + ) + return train_indices, test_indices -def mergeandsplit(config, trainindex=0, uniform=True, windows2linux=False): - """ - 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. +def pad_train_test_indices(train_inds, test_inds, train_fraction): + n_train_inds = len(train_inds) + n_test_inds = len(test_inds) + index_len = n_train_inds + n_test_inds + if n_train_inds / index_len == train_fraction: + return - 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 corrensponding video from the config.yaml file as variable trainindex. + # Determine the index length required to guarantee + # the train–test ratio is exactly the desired one. + min_length_req = int(100 / math.gcd(100, int(round(100 * train_fraction)))) + min_n_train = int(round(min_length_req * train_fraction)) + min_n_test = min_length_req - min_n_train + mult = max( + math.ceil(n_train_inds / min_n_train), + math.ceil(n_test_inds / min_n_test), + ) + n_train = mult * min_n_train + n_test = mult * min_n_test + # Pad indices so lengths agree + train_inds = np.append(train_inds, [-1] * (n_train - n_train_inds)) + test_inds = np.append(test_inds, [-1] * (n_test - n_test_inds)) + return train_inds, test_inds - Parameter - ---------- - config : string - 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. +def mergeandsplit(config: str | Path | ProjectConfig | dict, trainindex=0, uniform=True): + """This function allows additional control over "create_training_dataset". - uniform: bool, optional - Perform uniform split (disregarding folder structure in labeled data), or (if False) leave one folder out. + 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. - windows2linux: bool. - The annotation files contain path formated according to your operating system. If you label on windows - but train & evaluate on a unix system (e.g. ubunt, colab, Mac) set this variable to True to convert the paths. + 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. + + Args: + config (str | Path | ProjectConfig | dict): Full path of the config.yaml file. + Alternatively, a ProjectConfig object or a dictionary can be passed. + 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. + uniform (bool, optional): Perform uniform split (disregarding folder structure in + labeled data), or (if False) leave one folder out. + + Examples: + 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. 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, + ) - Examples - -------- - 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. - 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) + To freeze a (uniform) split (i.e. iid sampled from all the data): - To freeze a (uniform) split (i.e. iid sampled from all the data): - >>> trainIndices, testIndices=deeplabcut.mergeandsplit(config,trainindex=0,uniform=True) + 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. - >>> deeplabcut.create_training_dataset(config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices],testIndices=[testIndices, testIndices]) - -------- + 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) + cfg = ProjectConfig.from_any(config, repair_path=True) scorer = cfg["scorer"] project_path = cfg["project_path"] # Create path for training sets & store data there - trainingsetfolder = auxiliaryfunctions.GetTrainingSetFolder( - cfg - ) # Path concatenation OS platform independent - auxiliaryfunctions.attempttomakefolder( - Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True - ) - fn = os.path.join(project_path, trainingsetfolder, "CollectedData_" + cfg["scorer"]) + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) # Path concatenation OS platform independent + auxiliaryfunctions.attempt_to_make_folder(Path(project_path) / str(trainingsetfolder), recursive=True) + fn = str(Path(project_path) / trainingsetfolder / ("CollectedData_" + cfg["scorer"])) try: - Data = pd.read_hdf(fn + ".h5") + data = pd.read_hdf(fn + ".h5") + data = drop_likelihood_columns(data) except FileNotFoundError: - Data = merge_annotateddatasets( + data = merge_annotateddatasets( cfg, - Path(os.path.join(project_path, trainingsetfolder)), - windows2linux=windows2linux, + Path(project_path) / trainingsetfolder, ) - if Data is None: + if data is None: return [], [] - Data = Data[scorer] # extract labeled data + 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(range(len(Data.index)), trainFraction) + trainIndices, testIndices = SplitTrials( + range(len(data.index)), + trainFraction, + True, + ) else: # leave one folder out split videos = cfg["video_sets"].keys() test_video_name = [Path(i).stem for i in videos][trainindex] print("Excluding the following folder (from training):", test_video_name) trainIndices, testIndices = [], [] - for index, name in enumerate(Data.index): - # print(index,name.split(os.sep)[1]) - if test_video_name == name.split(os.sep)[1]: # this is the video name + for index, name in enumerate(data.index): + if test_video_name == name[1]: # this is the video name # print(name,test_video_name) testIndices.append(index) else: @@ -760,9 +708,12 @@ def mergeandsplit(config, trainindex=0, uniform=True, windows2linux=False): return trainIndices, testIndices -@lru_cache(maxsize=None) -def _read_image_shape_fast(path): - return io.imread(path).shape +@cache +def read_image_shape_fast(path): + # 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 def format_training_data(df, train_inds, nbodyparts, project_path): @@ -774,26 +725,49 @@ def to_matlab_cell(array): outer[0, 0] = array.astype("int64") return outer + # Again, remove likelihood if present + df = drop_likelihood_columns(df) + + if isinstance(df.columns, pd.MultiIndex): + coord_level = "coords" if "coords" in df.columns.names else df.columns.names[-1] + coord_values = df.columns.get_level_values(coord_level) + + has_x = "x" in coord_values + has_y = "y" in coord_values + + if not (has_x and has_y): + raise ValueError( + f"Training data must contain x/y coordinates. Found coordinate labels: {list(pd.unique(coord_values))}" + ) + for i in train_inds: data = dict() filename = df.index[i] data["image"] = filename - img_shape = _read_image_shape_fast(os.path.join(project_path, filename)) - try: - data["size"] = img_shape[2], img_shape[0], img_shape[1] - except IndexError: - data["size"] = 1, img_shape[0], img_shape[1] - temp = df.iloc[i].values.reshape(-1, 2) + img_shape = read_image_shape_fast(Path(project_path).joinpath(*filename)) + data["size"] = img_shape + + row = df.iloc[i].values + + if row.size % 2 != 0: + raise ValueError( + "Training data row does not contain an even number of coordinate values " + f"after dropping non-coordinate columns. Row size={row.size}, " + f"image={filename}" + ) + + temp = row.reshape(-1, 2) joints = np.c_[range(nbodyparts), temp] joints = joints[~np.isnan(joints).any(axis=1)].astype(int) - # Check that points lie within the image + inside = np.logical_and( - np.logical_and(joints[:, 1] < img_shape[1], joints[:, 1] > 0), - np.logical_and(joints[:, 2] < img_shape[0], joints[:, 2] > 0), + np.logical_and(joints[:, 1] < img_shape[2], joints[:, 1] > 0), + np.logical_and(joints[:, 2] < img_shape[1], joints[:, 2] > 0), ) if not all(inside): joints = joints[inside] - if joints.size: # Exclude images without labels + + if joints.size: data["joints"] = joints train_data.append(data) matlab_data.append( @@ -803,95 +777,264 @@ 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, + dtype=[("image", "O"), ("size", "O"), ("joints", "O")], ) return train_data, matlab_data def create_training_dataset( - config, + config: str | Path | ProjectConfig | dict, num_shuffles=1, Shuffles=None, windows2linux=False, - userfeedback=False, + userfeedback=True, trainIndices=None, testIndices=None, net_type=None, + detector_type=None, augmenter_type=None, + posecfg_template=None, + superanimal_name="", + weight_init: WeightInitialization | None = None, + engine: Engine | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): - """ - Creates a training dataset. 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_video' at any stage of the project to add more videos to the project. - - Parameter - ---------- - config : string - Full path of the config.yaml file as a string. - - num_shuffles : int, optional - Number of shuffles of training dataset to create, i.e. [1,2,3] for num_shuffles=3. Default is set to 1. + """Creates a training 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 (str | Path | ProjectConfig | dict): Full path of the ``config.yaml`` + file. Alternatively, a ProjectConfig object or a dictionary can be passed. + num_shuffles (int, optional): Number of shuffles of training dataset to create, + i.e. ``[1,2,3]`` for ``num_shuffles=3``. Defaults to 1. + Shuffles (list[int], optional): Alternatively the user can also give a list of + shuffles. + userfeedback (bool, optional): If ``False``, all requested train/test splits are + created (no matter if they already exist). If you want to assure that + previous splits etc. are not overwritten, set this to ``True`` and you will + be asked for each split. Defaults to True. + trainIndices (list of lists, optional): List of one or multiple lists containing + train indexes. A list containing two lists of training indexes will produce + two splits. Defaults to None. + testIndices (list of lists, optional): List of one or multiple lists containing + test indexes. Defaults to None. + net_type (list, optional): Type of networks. The options available depend on which + engine is used. Currently supported options are: + TensorFlow + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``mobilenet_v2_1.0`` + * ``mobilenet_v2_0.75`` + * ``mobilenet_v2_0.5`` + * ``mobilenet_v2_0.35`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` for + a complete list) + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_cspnext_m`` + * ``ctd_prenet_cspnext_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` + * ``dekr_w18`` + * ``dekr_w32`` + * ``dekr_w48`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` + * ``top_down_hrnet_w18`` + * ``top_down_hrnet_w32`` + * ``top_down_hrnet_w48`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` + Defaults to None. + + detector_type (string, optional): Only for the PyTorch engine. When passing + creating shuffles for top-down models, you can specify which detector you + want. If the detector_type is None, the ```ssdlite``` will be used. The list + of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported + options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` + Defaults to None. + + augmenter_type (string, optional): Type of augmenter. The options available + depend on which engine is used. Currently supported options are: + TensorFlow + * ``default`` + * ``scalecrop`` + * ``imgaug`` + * ``tensorpack`` + * ``deterministic`` + PyTorch + * ``albumentations`` + Defaults to None. + + posecfg_template (string, optional): Only for the TensorFlow engine. Path to a + ``pose_cfg.yaml`` file to use as a template for generating the new one for + the current iteration. Useful if you would like to start with the same + parameters a previous training iteration. None uses the default + ``pose_cfg.yaml``. Defaults to None. + + superanimal_name (string, optional): Only for the TensorFlow engine. For the + PyTorch engine, use the ``weight_init`` parameter. Specify the superanimal + name is transfer learning with superanimal is desired. This makes sure the + pose config template uses superanimal configs as template. Defaults to "". + + weight_init (WeightInitialisation, optional): PyTorch engine only. Specify how + model weights should be initialized. The default mode uses transfer learning + from ImageNet weights. Defaults to None. + + engine (Engine, optional): Whether to create a pose config for a Tensorflow or + PyTorch model. Defaults to the value specified in the project configuration + file. If no engine is specified for the project, defaults to + ``deeplabcut.compat.DEFAULT_ENGINE``. + + ctd_conditions (int | str | Path | tuple[int, str] | tuple[int, int] | None, + optional): If using a conditional-top-down (CTD) net_type, this argument + should be specified. 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. Valid for both evaluation and live + analyze. + * A predictions file path (ctd_conditions: string | Path), which must + correspond to a .json or .h5 predictions file. Evaluation-only — + not valid for ``analyze_images`` / ``analyze_videos``. + * 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. Defaults to None. + + Returns: + list(tuple) or None: If training dataset was successfully created, a list of + tuples is returned. The first two elements in each tuple represent the + training fraction and the shuffle value. The last two elements in each tuple + are arrays of integers representing the training and test indices. + + Returns None if training dataset could not be created. + + Note: + Use the function ``add_new_videos`` at any stage of the project to add more + videos to the project. + + Examples: + Linux/MacOS: + deeplabcut.create_training_dataset( + '/analysis/project/reaching-task/config.yaml', num_shuffles=1, + ) - Shuffles: list of shuffles. - Alternatively the user can also give a list of shuffles (integers!). + deeplabcut.create_training_dataset( + '/analysis/project/reaching-task/config.yaml', Shuffles=[2], engine=deeplabcut.Engine.TF, + ) - windows2linux: bool. - The annotation files contain path formated according to your operating system. If you label on windows - but train & evaluate on a unix system (e.g. ubunt, colab, Mac) set this variable to True to convert the paths. + Windows: - userfeedback: bool, optional - If this is set to false, then all requested train/test splits are created (no matter if they already exist). If you - want to assure that previous splits etc. are not overwritten, then set this to True and you will be asked for each split. + deeplabcut.create_training_dataset( + "C:\\Users\\Ulf\\looming-task\\config.yaml", + Shuffles=[3, 17, 5], + ) + """ + import scipy.io as sio - trainIndices: list of lists, optional (default=None) - List of one or multiple lists containing train indexes. - A list containing two lists of training indexes will produce two splits. + if windows2linux: + # DeprecationWarnings are silenced since Python 3.2 unless triggered in __main__ + warnings.warn( + "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", + FutureWarning, + stacklevel=2, + ) - testIndices: list of lists, optional (default=None) - List of one or multiple lists containing test indexes. + # Loading metadata from config file: + cfg = ProjectConfig.from_any(config, repair_path=True) + cfg_path = config if isinstance(config, (str, Path)) else cfg.config_yaml_path - net_type: list - Type of networks. Currently resnet_50, resnet_101, resnet_152, mobilenet_v2_1.0, mobilenet_v2_0.75, - mobilenet_v2_0.5, mobilenet_v2_0.35, efficientnet-b0, efficientnet-b1, efficientnet-b2, efficientnet-b3, - efficientnet-b4, efficientnet-b5, and efficientnet-b6 are supported. + auxiliaryfunctions.get_deeplabcut_path() - augmenter_type: string - Type of augmenter. Currently default, imgaug, tensorpack, and deterministic are supported. + if superanimal_name != "": + raise ValueError( + "Invalid argument superanimal_name. This functionality has been " + "removed. Please use modelzoo.build_weight_init() instead." + ) - Example - -------- - >>> deeplabcut.create_training_dataset('/analysis/project/reaching-task/config.yaml',num_shuffles=1) - Windows: - >>> deeplabcut.create_training_dataset('C:\\Users\\Ulf\\looming-task\\config.yaml',Shuffles=[3,17,5]) - -------- - """ - import scipy.io as sio + if posecfg_template: + posecfg_template = Path(posecfg_template) + if posecfg_template.name not in {"pose_cfg.yaml", "superquadruped.yaml", "supertopview.yaml"}: + raise ValueError("posecfg_template argument must contain path to a pose_cfg.yaml file") + else: + print(f"Reloading pose_cfg parameters from {posecfg_template}\n") + from deeplabcut.utils.auxiliaryfunctions import read_plainconfig - # Loading metadata from config file: - cfg = auxiliaryfunctions.read_config(config) + prior_cfg = read_plainconfig(posecfg_template) if cfg.get("multianimalproject", False): from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import ( create_multianimaltraining_dataset, ) create_multianimaltraining_dataset( - config, num_shuffles, Shuffles, windows2linux, net_type + cfg, + num_shuffles, + Shuffles, + net_type=net_type, + detector_type=detector_type, + trainIndices=trainIndices, + testIndices=testIndices, + userfeedback=userfeedback, + engine=engine, + weight_init=weight_init, + ctd_conditions=ctd_conditions, ) else: scorer = cfg["scorer"] project_path = cfg["project_path"] + if engine is None: + engine = compat.get_project_engine(cfg) + # Create path for training sets & store data there - trainingsetfolder = auxiliaryfunctions.GetTrainingSetFolder( + trainingsetfolder = auxiliaryfunctions.get_training_set_folder( cfg ) # Path concatenation OS platform independent - auxiliaryfunctions.attempttomakefolder( - Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True - ) + auxiliaryfunctions.attempt_to_make_folder(Path(project_path) / str(trainingsetfolder), recursive=True) + + # Create the trainset metadata file, if it doesn't yet exist + if not metadata.TrainingDatasetMetadata.path(cfg).exists(): + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + trainset_metadata.save() Data = merge_annotateddatasets( - cfg, Path(os.path.join(project_path, trainingsetfolder)), windows2linux + cfg, + Path(project_path) / trainingsetfolder, ) if Data is None: return @@ -900,47 +1043,73 @@ def create_training_dataset( # loading & linking pretrained models if net_type is None: # loading & linking pretrained models net_type = cfg.get("default_net_type", "resnet_50") + elif engine == Engine.PYTORCH: + pass else: - if ( - "resnet" in net_type - or "mobilenet" in net_type - or "efficientnet" 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) + top_down = False + if engine == Engine.PYTORCH: + if net_type.startswith("top_down_"): + top_down = True + net_type = net_type[len("top_down_") :] + + augmenters = compat.get_available_aug_methods(engine) + default_augmenter = augmenters[0] if augmenter_type is None: - augmenter_type = cfg.get("default_augmenter", "imgaug") + augmenter_type = cfg.get("default_augmenter", default_augmenter) + if augmenter_type is None: # this could be in config.yaml for old projects! # updating variable if null/None! #backwardscompatability - auxiliaryfunctions.edit_config(config, {"default_augmenter": "imgaug"}) - augmenter_type = "imgaug" - else: - if augmenter_type in [ - "default", - "scalecrop", - "imgaug", - "tensorpack", - "deterministic", - ]: - pass - else: - raise ValueError("Invalid augmenter type:", augmenter_type) + augmenter_type = default_augmenter + cfg.default_augmenter = augmenter_type + cfg.to_yaml(cfg_path, log_changes=True, mark_clean=True) + elif augmenter_type not in augmenters: + # as the default augmenter might not be available for the given engine + augmenter_type = default_augmenter + logging.info( + f"Default augmenter {augmenter_type} not available for engine " + f"{engine}: using {default_augmenter} instead" + ) + + if augmenter_type not in augmenters: + if engine != Engine.PYTORCH: + raise ValueError( + f"Invalid augmenter type: {augmenter_type} (available: for engine={engine}: {augmenters})" + ) + + logging.info(f"Switching augmentation to {default_augmenter} for PyTorch") + augmenter_type = default_augmenter + + 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." + ) + 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." + ) # Loading the encoder (if necessary downloading from TF) dlcparent_path = auxiliaryfunctions.get_deeplabcut_path() - defaultconfigfile = os.path.join(dlcparent_path, "pose_cfg.yaml") - model_path, num_shuffles = auxfun_models.Check4weights( - net_type, Path(dlcparent_path), num_shuffles - ) + if not posecfg_template: + defaultconfigfile = dlcparent_path / "pose_cfg.yaml" + elif posecfg_template: + defaultconfigfile = posecfg_template - if Shuffles is None: - Shuffles = range(1, num_shuffles + 1) + if engine == Engine.PYTORCH: + model_path = dlcparent_path else: - Shuffles = [i for i in Shuffles if isinstance(i, int)] + model_path = auxfun_models.check_for_weights(net_type, dlcparent_path) + + Shuffles = validate_shuffles(cfg, Shuffles, num_shuffles, userfeedback) - # print(trainIndices,testIndices, Shuffles, augmenter_type,net_type) if trainIndices is None and testIndices is None: splits = [ ( @@ -953,45 +1122,40 @@ 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)}%" - ) - splits.append( - (trainFraction, Shuffles[shuffle], (train_inds, test_inds)) - ) - - bodyparts = cfg["bodyparts"] + 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, + # 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))) + + bodyparts = auxiliaryfunctions.get_bodyparts(cfg) nbodyparts = len(bodyparts) for trainFraction, shuffle, (trainIndices, testIndices) in splits: if len(trainIndices) > 0: if userfeedback: - trainposeconfigfile, _, _ = training.return_train_network_path( - config, + trainposeconfigfile, _, _ = compat.return_train_network_path( + cfg_path, shuffle=shuffle, trainingsetindex=cfg["TrainingFraction"].index(trainFraction), + engine=engine, ) 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" - ): + 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." ) #################################################### @@ -1001,179 +1165,352 @@ def create_training_dataset( ( datafilename, metadatafilename, - ) = auxiliaryfunctions.GetDataandMetaDataFilenames( - 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(str(Path(project_path) / datafilename), {"dataset": MatlabData}) ################################################################################ # Saving metadata (Pickle file) ################################################################################ - auxiliaryfunctions.SaveMetadata( - os.path.join(project_path, metadatafilename), + auxiliaryfunctions.save_metadata( + Path(project_path) / metadatafilename, data, trainIndices, testIndices, trainFraction, ) + metadata.update_metadata( + cfg=cfg, + train_fraction=trainFraction, + shuffle=shuffle, + engine=engine, + train_indices=trainIndices, + test_indices=testIndices, + overwrite=not userfeedback, + ) ################################################################################ # Creating file structure for training & # Test files as well as pose_yaml files (containing training and testing information) ################################################################################# - modelfoldername = auxiliaryfunctions.GetModelFolder( - trainFraction, shuffle, cfg - ) - auxiliaryfunctions.attempttomakefolder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempttomakefolder( - str(Path(config).parents[0] / modelfoldername) + "/train" - ) - auxiliaryfunctions.attempttomakefolder( - str(Path(config).parents[0] / modelfoldername) + "/test" + modelfoldername = auxiliaryfunctions.get_model_folder( + trainFraction, + shuffle, + cfg, + engine=engine, ) + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername / "train") + auxiliaryfunctions.attempt_to_make_folder(cfg.project_path / modelfoldername / "test") + + path_train_config = str(Path(cfg["project_path"]) / modelfoldername / "train" / engine.pose_cfg_name) + path_test_config = str(Path(cfg["project_path"]) / modelfoldername / "test" / "pose_cfg.yaml") + if engine == Engine.TF: + if weight_init is not None: + raise ValueError( + "Weight initialization is not supported for TensorFlow engine. " + "Pretrained weights are automatically downloaded." + ) + items2change = { + "dataset": datafilename, + "engine": engine.aliases[0], + "metadataset": metadatafilename, + "num_joints": len(bodyparts), + "all_joints": [[i] for i in range(len(bodyparts))], + "all_joints_names": [str(bpt) for bpt in bodyparts], + "init_weights": model_path, + "project_path": str(cfg["project_path"]), + "net_type": net_type, + "dataset_type": augmenter_type, + } - path_train_config = str( - os.path.join( - cfg["project_path"], - Path(modelfoldername), - "train", - "pose_cfg.yaml", + items2drop = {} + if augmenter_type == "scalecrop": + # these values are dropped as scalecrop + # doesn't have rotation implemented + items2drop = {"rotation": 0, "rotratio": 0.0} + # Also drop maDLC smart cropping augmentation parameters + for key in [ + "pre_resize", + "crop_size", + "max_shift", + "crop_sampling", + ]: + items2drop[key] = None + + trainingdata = MakeTrain_pose_yaml( + items2change, + path_train_config, + defaultconfigfile, + items2drop, + save=(engine == Engine.TF), ) - ) - path_test_config = str( - os.path.join( - cfg["project_path"], - Path(modelfoldername), - "test", - "pose_cfg.yaml", + + keys2save = [ + "dataset", + "num_joints", + "all_joints", + "all_joints_names", + "net_type", + "init_weights", + "global_scale", + "location_refinement", + "locref_stdev", + ] + MakeTest_pose_yaml(trainingdata, keys2save, path_test_config) + print( + "The training dataset is successfully created. Use the function" + "'train_network' to start training. Happy training!" + ) + elif engine == Engine.PYTORCH: + from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_pytorch_pose_config, + make_pytorch_test_config, + ) + from deeplabcut.pose_estimation_pytorch.modelzoo.config import ( + make_super_animal_finetune_config, ) - ) - # str(cfg['proj_path']+'/'+Path(modelfoldername) / 'test' / 'pose_cfg.yaml') - items2change = { - "dataset": datafilename, - "metadataset": metadatafilename, - "num_joints": len(bodyparts), - "all_joints": [[i] for i in range(len(bodyparts))], - "all_joints_names": [str(bpt) for bpt in bodyparts], - "init_weights": model_path, - "project_path": str(cfg["project_path"]), - "net_type": net_type, - "dataset_type": augmenter_type, - } - - items2drop = {} - if augmenter_type == "scalecrop": - # these values are dropped as scalecrop - # doesn't have rotation implemented - items2drop = {"rotation": 0, "rotratio": 0.0} - - trainingdata = MakeTrain_pose_yaml( - items2change, path_train_config, defaultconfigfile, items2drop - ) - keys2save = [ - "dataset", - "num_joints", - "all_joints", - "all_joints_names", - "net_type", - "init_weights", - "global_scale", - "location_refinement", - "locref_stdev", - ] - MakeTest_pose_yaml(trainingdata, keys2save, path_test_config) - print( - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!" - ) + if weight_init is not None and weight_init.with_decoder: + pytorch_cfg = make_super_animal_finetune_config( + project_config=cfg, + pose_config_path=path_train_config, + model_name=net_type, + detector_name=detector_type, + weight_init=weight_init, + save=True, + ) + else: + pytorch_cfg = make_pytorch_pose_config( + project_config=cfg, + pose_config_path=path_train_config, + net_type=net_type, + top_down=top_down, + detector_type=detector_type, + weight_init=weight_init, + save=True, + ctd_conditions=ctd_conditions, + ) + + make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) + return splits -def get_largestshuffle_index(config): - """ Returns the largest shuffle for all dlc-models in the current iteration.""" - cfg = auxiliaryfunctions.read_config(config) - project_path = cfg["project_path"] - iterate = "iteration-" + str(cfg["iteration"]) - dlc_model_path = os.path.join(project_path, "dlc-models", iterate) - if os.path.isdir(dlc_model_path): - models = os.listdir(dlc_model_path) - # sort the models directories - models.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) - # get the shuffle index - max_shuffle_index = int(models[-1].split("shuffle")[-1]) - else: - max_shuffle_index = 0 - return max_shuffle_index +def get_largestshuffle_index(config: str | Path): + """Returns the largest shuffle for all dlc-models in the current iteration.""" + shuffle_indices = get_existing_shuffle_indices(config) + if len(shuffle_indices) > 0: + return shuffle_indices[-1] + return None -def create_training_model_comparison( - config, - trainindex=0, - num_shuffles=1, - net_types=["resnet_50"], - augmenter_types=["default"], - userfeedback=False, - windows2linux=False, -): + +def get_existing_shuffle_indices( + cfg: dict | str | Path, + train_fraction: float | None = None, + engine: Engine | None = None, +) -> list[int]: + """Get the existing shuffle indices. + + Args: + cfg: The content of a project configuration file, or the path to the project + configuration file. + train_fraction: If defined, only get the indices of shuffles with this train + fraction. + engine: If specified, returns only the shuffle indices that were created with + the given engine. Can only be used when train_fraction is also defined. + + Returns: + the indices of existing shuffles for this iteration of the project, sorted by + ascending index """ - Creates a training dataset with different networks and augmentation types (dataset_loader) so that the shuffles - have same training and testing indices. - Therefore, this function is useful for benchmarking the performance of different network and augmentation types on the same training/testdata.\n + def is_valid_data_stem(stem: str) -> bool: + if len(stem) == 0: + return False + suffix = stem.split("_")[-1] + if len(suffix) == 0: + return False + info = suffix.split("shuffle") + if len(info) != 2: + return False + train_frac, idx = info + return ( + train_frac.isdigit() + and idx.isdigit() + and (train_fraction is None or int(train_frac) == int(100 * train_fraction)) + ) - Parameter - ---------- - config : string - Full path of the config.yaml file as a string. + if isinstance(cfg, (str, Path)): + cfg = read_config(cfg) - 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. + project = Path(cfg["project_path"]) + trainset_folder = project / auxiliaryfunctions.get_training_set_folder(cfg) + if not trainset_folder.exists(): + return [] - num_shuffles : int, optional - Number of shuffles of training dataset to create, i.e. [1,2,3] for num_shuffles=3. Default is set to 1. + 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 engine is not None: + if train_fraction is None: + raise ValueError(f"Must select {train_fraction} to filter shuffles by engine") - net_types: list - Type of networks. Currently resnet_50, resnet_101, resnet_152, mobilenet_v2_1.0,mobilenet_v2_0.75, mobilenet_v2_0.5, mobilenet_v2_0.35, - efficientnet-b0, efficientnet-b1, efficientnet-b2, efficientnet-b3, efficientnet-b4, - efficientnet-b5, and efficientnet-b6 are supported. + shuffle_indices = [ + idx + for idx in shuffle_indices + if ( + project + / auxiliaryfunctions.get_model_folder( + trainFraction=train_fraction, + shuffle=idx, + cfg=cfg, + engine=engine, + ) + ).exists() + ] - augmenter_types: list - Type of augmenters. Currently "default", "imgaug", "tensorpack", and "deterministic" are supported. + return sorted(shuffle_indices) - userfeedback: bool, optional - If this is set to false, then all requested train/test splits are created (no matter if they already exist). If you - want to assure that previous splits etc. are not overwritten, then set this to True and you will be asked for each split. - windows2linux: bool. - The annotation files contain path formated according to your operating system. If you label on windows - but train & evaluate on a unix system (e.g. ubunt, colab, Mac) set this variable to True to convert the paths. +def validate_shuffles( + cfg: dict, + shuffles: list[int] | None, + num_shuffles: int | None, + userfeedback: bool, +) -> list[int]: + existing_shuffles = get_existing_shuffle_indices(cfg) + if shuffles is None: + first_index = 1 + if len(existing_shuffles) > 0: + first_index = existing_shuffles[-1] + 1 - Example - -------- - >>> deeplabcut.create_training_model_comparison('/analysis/project/reaching-task/config.yaml',num_shuffles=1,net_types=['resnet_50','resnet_152'],augmenter_types=['tensorpack','deterministic']) + shuffles = range(first_index, num_shuffles + first_index) + else: + shuffles = [i for i in shuffles if isinstance(i, int)] + for shuffle_idx in shuffles: + if userfeedback and shuffle_idx in existing_shuffles: + raise ValueError( + f"Cannot create shuffle {shuffle_idx} as it already exists - " + f"you must either create the dataset with `userfeedback=False` " + f"or delete the shuffle with index {shuffle_idx} manually (in " + f"`dlc-models`/`dlc-models-pytorch` and in the " + f"`training-datasets` folder) if you want to create a new " + f"shuffle with that index. You can otherwise create a shuffle " + f"with a new index. Existing indices are {existing_shuffles}." + ) + + return shuffles - Windows: - >>> deeplabcut.create_training_model_comparison('C:\\Users\\Ulf\\looming-task\\config.yaml',num_shuffles=1,net_types=['resnet_50','resnet_152'],augmenter_types=['tensorpack','deterministic']) - -------- +def create_training_model_comparison( + config: str | Path | ProjectConfig | dict, + trainindex=0, + num_shuffles=1, + net_types=None, + augmenter_types=None, + userfeedback=False, + windows2linux=False, +): + """Creates a training dataset to compare networks and augmentation types. + + The datasets are created such that the shuffles have same training and testing + indices. Therefore, this function is useful for benchmarking the performance of + different network and augmentation types on the same training/testdata. + + Args: + config (str | Path | ProjectConfig | dict): Full path of the config.yaml file. + Alternatively, a ProjectConfig object or a dictionary can be passed. + 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. Defaults to 0. + num_shuffles (int, optional): Number of shuffles of training dataset to create, + i.e. [1,2,3] for num_shuffles=3. Defaults to 1. + net_types (list[str], optional): Currently supported networks are + + * ``"resnet_50"`` + * ``"resnet_101"`` + * ``"resnet_152"`` + * ``"mobilenet_v2_1.0"`` + * ``"mobilenet_v2_0.75"`` + * ``"mobilenet_v2_0.5"`` + * ``"mobilenet_v2_0.35"`` + * ``"efficientnet-b0"`` + * ``"efficientnet-b1"`` + * ``"efficientnet-b2"`` + * ``"efficientnet-b3"`` + * ``"efficientnet-b4"`` + * ``"efficientnet-b5"`` + * ``"efficientnet-b6"`` + + Defaults to ["resnet_50"]. + + augmenter_types (list[str], optional): Currently supported augmenters are + + * ``"default"`` + * ``"imgaug"`` + * ``"tensorpack"`` + * ``"deterministic"`` + + Defaults to ["imgaug"]. + + userfeedback (bool, optional): If ``False``, then all requested train/test splits + are created, no matter if they already exist. If you want to assure that + previous splits etc. are not overwritten, then set this to True and you will + be asked for each split. Defaults to False. + windows2linux: ..deprecated:: Has no effect since 2.2.0.4 and will be removed in + 2.2.1. + + Returns: + shuffle_list (list): List of indices corresponding to the trainingsplits/models + that were created. + + Examples: + On Linux/MacOS + + shuffle_list = deeplabcut.create_training_model_comparison( + '/analysis/project/reaching-task/config.yaml', + num_shuffles=1, + net_types=['resnet_50','resnet_152'], + augmenter_types=['tensorpack','deterministic'], + ) + + On Windows + + shuffle_list = deeplabcut.create_training_model_comparison( + 'C:\\Users\\Ulf\\looming-task\\config.yaml', + num_shuffles=1, + net_types=['resnet_50','resnet_152'], + augmenter_types=['tensorpack','deterministic'], + ) + + See ``examples/testscript_openfielddata_augmentationcomparison.py`` for an + example of how to use ``shuffle_list``. """ # read cfg file - cfg = auxiliaryfunctions.read_config(config) + if augmenter_types is None: + augmenter_types = ["imgaug"] + if net_types is None: + net_types = ["resnet_50"] + cfg = ProjectConfig.from_any(config, repair_path=True) + + 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 - log_file_name = os.path.join(cfg["project_path"], "training_model_comparison.log") + log_file_name = str(Path(cfg["project_path"]) / "training_model_comparison.log") logger = logging.getLogger("training_model_comparison") if not logger.handlers: logger = logging.getLogger("training_model_comparison") @@ -1185,12 +1522,15 @@ def create_training_model_comparison( else: pass - largestshuffleindex = get_largestshuffle_index(config) + existing_shuffles = get_existing_shuffle_indices(cfg) + if len(existing_shuffles) == 0: + largestshuffleindex = 0 + else: + largestshuffleindex = existing_shuffles[-1] + 1 + shuffle_list = [] for shuffle in range(num_shuffles): - trainIndices, testIndices = mergeandsplit( - config, trainindex=trainindex, uniform=True - ) + trainIndices, testIndices = mergeandsplit(cfg, trainindex=trainindex, uniform=True) for idx_net, net in enumerate(net_types): for idx_aug, aug in enumerate(augmenter_types): get_max_shuffle_idx = ( @@ -1199,6 +1539,8 @@ def create_training_model_comparison( + idx_net * len(augmenter_types) + shuffle * len(augmenter_types) * len(net_types) ) + + shuffle_list.append(get_max_shuffle_idx) log_info = str( "Shuffle index:" + str(get_max_shuffle_idx) @@ -1208,15 +1550,211 @@ def create_training_model_comparison( + aug + ", trainsetindex:" + str(trainindex) + + ", frozen shuffle ID:" + + str(shuffle) ) create_training_dataset( - config, + cfg, Shuffles=[get_max_shuffle_idx], net_type=net, trainIndices=[trainIndices], testIndices=[testIndices], augmenter_type=aug, userfeedback=userfeedback, - windows2linux=windows2linux, ) logger.info(log_info) + + return shuffle_list + + +def create_training_dataset_from_existing_split( + config: str | Path | ProjectConfig | dict, + from_shuffle: int, + from_trainsetindex: int = 0, + num_shuffles: int = 1, + shuffles: list[int] | None = None, + userfeedback: bool = True, + net_type: str | None = None, + detector_type: str | None = None, + augmenter_type: str | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, + posecfg_template: dict | None = None, + superanimal_name: str = "", + 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. + + Args: + config (str | Path | ProjectConfig | dict): + Full path of the ``config.yaml`` file. Alternatively, a ProjectConfig object or a dictionary can be passed. + + from_shuffle: The index of the shuffle from which to copy the train/test split. + + from_trainsetindex: The trainset index of the shuffle from which to use the data + split. Default is 0. + + num_shuffles: Number of shuffles of training dataset to create, used if + ``shuffles`` is None. + + shuffles: If defined, ``num_shuffles`` is ignored and a shuffle is created for + each index given in the list. + + userfeedback: If ``False``, all requested train/test splits are created (no + matter if they already exist). If you want to assure that previous splits + etc. are not overwritten, set this to ``True`` and you will be asked for + each existing split if you want to overwrite it. + + net_type: The type of network to create the shuffle for. Currently supported + options for engine=Engine.TF are: + * ``resnet_50`` + * ``resnet_101`` + * ``resnet_152`` + * ``mobilenet_v2_1.0`` + * ``mobilenet_v2_0.75`` + * ``mobilenet_v2_0.5`` + * ``mobilenet_v2_0.35`` + * ``efficientnet-b0`` + * ``efficientnet-b1`` + * ``efficientnet-b2`` + * ``efficientnet-b3`` + * ``efficientnet-b4`` + * ``efficientnet-b5`` + * ``efficientnet-b6`` + Currently supported options for engine=Engine.TF can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_models()``. + + detector_type: string, optional, default=None + Only for the PyTorch engine. + When passing creating shuffles for top-down models, you can specify which + detector you want. If the detector_type is None, the ```ssdlite``` will be + used. The list of all available detectors can be obtained by calling + ``deeplabcut.pose_estimation_pytorch.available_detectors()``. Supported + options: + * ``ssdlite`` + * ``fasterrcnn_mobilenet_v3_large_fpn`` + * ``fasterrcnn_resnet50_fpn_v2`` + + augmenter_type: Type of augmenter. Currently supported augmenters for + engine=Engine.TF are + * ``default`` + * ``scalecrop`` + * ``imgaug`` + * ``tensorpack`` + * ``deterministic`` + The only supported augmenter for Engine.PYTORCH is ``albumentations``. + + posecfg_template: Only for Engine.TF. Path to a ``pose_cfg.yaml`` file to use as + a template for generating the new one for the current iteration. Useful if + you would like to start with the same parameters a previous training + iteration. None uses the default ``pose_cfg.yaml``. + + superanimal_name: Specify the superanimal name is transfer learning with + superanimal is desired. This makes sure the pose config template uses + superanimal configs as template. + + weight_init: Only for Engine.PYTORCH. Specify how model weights should be + initialized. The default mode uses transfer learning from ImageNet weights. + + engine: Whether to create a pose config for a Tensorflow or PyTorch model. + Defaults to the value specified in the project configuration file. If no + engine is specified for the project, defaults to + ``deeplabcut.compat.DEFAULT_ENGINE``. + + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None, default = None, + If using a conditional-top-down (CTD) net_type, this argument should be + specified. 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. Valid for both evaluation and live + analyze. + * A predictions file path (ctd_conditions: string | Path), which must + correspond to a .json or .h5 predictions file. Evaluation-only — + not valid for ``analyze_images`` / ``analyze_videos``. + * 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: + If training dataset was successfully created, a list of tuples is returned. + The first two elements in each tuple represent the training fraction and the + shuffle value. The last two elements in each tuple are arrays of integers + representing the training and test indices. + + Returns None if training dataset could not be created. + + Raises: + ValueError: If the shuffle from which to copy the data split doesn't exist. + """ + cfg = ProjectConfig.from_any(config, repair_path=True) + trainset_meta_path = metadata.TrainingDatasetMetadata.path(cfg) + if not trainset_meta_path.exists(): + meta = metadata.TrainingDatasetMetadata.create(cfg) + meta.save() + else: + meta = metadata.TrainingDatasetMetadata.load(cfg, load_splits=False) + + shuffle = meta.get(trainset_index=from_trainsetindex, index=from_shuffle) + shuffle = shuffle.load_split(cfg, trainset_path=trainset_meta_path.parent) + + num_copies = num_shuffles + if shuffles is not None: + num_copies = len(shuffles) + + # pad the train and test indices with -1s so the training fraction is exact + train_idx = list(shuffle.split.train_indices) + test_idx = list(shuffle.split.test_indices) + n_train, n_test = len(train_idx), len(test_idx) + + train_fraction = round(cfg["TrainingFraction"][from_trainsetindex], 2) + if round(n_train / (n_train + n_test), 2) != train_fraction: + train_padding, test_padding = _compute_padding(train_fraction, n_train, n_test) + train_idx = train_idx + (train_padding * [-1]) + test_idx = test_idx + (test_padding * [-1]) + + return create_training_dataset( + config=cfg, + num_shuffles=num_shuffles, + Shuffles=shuffles, + userfeedback=userfeedback, + trainIndices=[train_idx for _ in range(num_copies)], + testIndices=[test_idx for _ in range(num_copies)], + net_type=net_type, + detector_type=detector_type, + augmenter_type=augmenter_type, + posecfg_template=posecfg_template, + superanimal_name=superanimal_name, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + + +def _compute_padding( + train_fraction: float, + num_train: int, + num_test: int, +) -> tuple[int, int]: + """Computes the amount of padding to add to train/test indices such that + train_fraction = num_train / (num_train + num_test). + + Returns: + the number of padding indices to add to the train indices + 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 {train_fraction} was found") + + base_images = 100 + train_step = int(round(round(train_fraction, 2) * base_images)) + test_step = base_images - train_step + + tgt_train = train_step + tgt_test = test_step + while tgt_train < num_train or tgt_test < num_test: + tgt_train += train_step + tgt_test += test_step + + return (tgt_train - num_train), (tgt_test - num_test) diff --git a/deeplabcut/gui/__init__.py b/deeplabcut/gui/__init__.py index af6907e9fc..e4a425ff56 100644 --- a/deeplabcut/gui/__init__.py +++ b/deeplabcut/gui/__init__.py @@ -1,17 +1,18 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 - -""" import os -from deeplabcut.utils.auxiliaryfunctions import get_deeplabcut_path +from pathlib import Path +os.environ["QT_API"] = "pyside6" +import qtpy # Necessary unused import to properly store the env variable -DLC_PATH = get_deeplabcut_path() -MEDIA_PATH = os.path.join(DLC_PATH, "gui", "media") -LOGO_PATH = os.path.join(MEDIA_PATH, "logo.png") +BASE_DIR = Path(__file__).parent diff --git a/deeplabcut/gui/analyze_videos.py b/deeplabcut/gui/analyze_videos.py deleted file mode 100644 index d051cca736..0000000000 --- a/deeplabcut/gui/analyze_videos.py +++ /dev/null @@ -1,563 +0,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 -import platform -import pydoc -import subprocess -import sys -import webbrowser - -import deeplabcut -import wx -from deeplabcut.utils import auxiliaryfunctions -from deeplabcut.gui import LOGO_PATH - - -class Analyze_videos(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - # variable initilization - self.filelist = [] - self.picklelist = [] - self.bodyparts = [] - self.config = cfg - self.cfg = auxiliaryfunctions.read_config(self.config) - self.draw = False - # design the panel - self.sizer = wx.GridBagSizer(5, 10) - - if self.cfg.get("multianimalproject", False): - text = wx.StaticText( - self, label="DeepLabCut - Step 7. Analyze Videos and Detect Tracklets" - ) - else: - text = wx.StaticText(self, label="DeepLabCut - Step 7. Analyze Videos ....") - - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - self.sizer.Add( - icon, pos=(0, 8), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5 - ) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 8), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=10) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - self.vids = wx.StaticText(self, label="Choose the videos") - self.sizer.Add(self.vids, pos=(3, 0), flag=wx.TOP | wx.LEFT, border=10) - - self.sel_vids = wx.Button(self, label="Select videos to analyze") - self.sizer.Add(self.sel_vids, pos=(3, 1), flag=wx.TOP | wx.EXPAND, border=5) - self.sel_vids.Bind(wx.EVT_BUTTON, self.select_videos) - - sb = wx.StaticBox(self, label="Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox2 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox3 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox4 = wx.BoxSizer(wx.HORIZONTAL) - - - videotype_text = wx.StaticBox(self, label="Specify the videotype") - videotype_text_boxsizer = wx.StaticBoxSizer(videotype_text, wx.VERTICAL) - videotypes = [".avi", ".mp4", ".mov"] - self.videotype = wx.ComboBox(self, choices=videotypes, style=wx.CB_READONLY) - self.videotype.SetValue(".avi") - videotype_text_boxsizer.Add( - self.videotype, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 1 - ) - - shuffle_text = wx.StaticBox(self, label="Specify the shuffle") - shuffle_boxsizer = wx.StaticBoxSizer(shuffle_text, wx.VERTICAL) - self.shuffle = wx.SpinCtrl(self, value="1", min=0, max=100) - shuffle_boxsizer.Add(self.shuffle, 1,wx.EXPAND | wx.TOP | wx.BOTTOM, 1) - - trainingset = wx.StaticBox(self, label="Specify the trainingset index") - trainingset_boxsizer = wx.StaticBoxSizer(trainingset, wx.VERTICAL) - self.trainingset = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingset_boxsizer.Add(self.trainingset, 1,wx.EXPAND | wx.TOP | wx.BOTTOM, 1) - - - self.hbox1.Add(videotype_text_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM,1) - self.hbox1.Add(shuffle_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 1) - self.hbox1.Add(trainingset_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM,1) - - if self.cfg.get("multianimalproject", False): - - self.robust = wx.RadioBox( - self, - label="Use ffprobe to read video metadata (slow but robust)", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.robust.SetSelection(1) - self.hbox2.Add(self.robust, 1, 1) - - self.create_video_with_all_detections = wx.RadioBox( - self, - label="Create video for checking detections", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.create_video_with_all_detections.SetSelection(1) - self.hbox2.Add( - self.create_video_with_all_detections, - 1, - wx.EXPAND | wx.TOP | wx.BOTTOM, - 1, - ) - - tracker_text = wx.StaticBox( - self, label="Specify the Tracker Method (you can try each)" - ) - tracker_text_boxsizer = wx.StaticBoxSizer(tracker_text, wx.VERTICAL) - trackertypes = ["skeleton", "box", "ellipse"] - self.trackertypes = wx.ComboBox( - self, choices=trackertypes, style=wx.CB_READONLY - ) - self.trackertypes.SetValue("ellipse") - tracker_text_boxsizer.Add( - self.trackertypes, 1,wx.EXPAND | wx.TOP | wx.BOTTOM,1 - ) - self.hbox3.Add(tracker_text_boxsizer, 1, 1) - - self.overwrite = wx.RadioBox( - self, - label="Overwrite tracking files (set to yes if you edit inference parameters)", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.overwrite.SetSelection(1) - self.hbox3.Add(self.overwrite, 1, 1) - - self.calibrate = wx.RadioBox( - self, - label="Calibrate animal assembly?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.calibrate.SetSelection(1) - self.hbox4.Add(self.calibrate, 1, 1) - - winsize_text = wx.StaticBox(self, label="Prioritize past connections over a window of size:") - winsize_sizer = wx.StaticBoxSizer(winsize_text, wx.VERTICAL) - self.winsize = wx.SpinCtrl(self, value="0") - winsize_sizer.Add(self.winsize, 1, wx.EXPAND | wx.TOP | wx.BOTTOM,1) - self.hbox4.Add(winsize_sizer, 1, 1) - - else: - self.csv = wx.RadioBox( - self, - label="Want to save result(s) as csv?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.csv.SetSelection(1) - - self.dynamic = wx.RadioBox( - self, - label="Want to dynamically crop bodyparts?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.dynamic.SetSelection(1) - - self.filter = wx.RadioBox( - self, - label="Want to filter the predictions?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.filter.SetSelection(1) - - self.trajectory = wx.RadioBox( - self, - label="Want to plot the trajectories?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - - self.showfigs = wx.RadioBox( - self, - label="Want plots to pop up?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - - self.trajectory.Bind(wx.EVT_RADIOBOX, self.chooseOption) - self.trajectory.SetSelection(1) - - self.hbox2.Add(self.csv, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox2.Add(self.filter, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox2.Add(self.showfigs, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.hbox3.Add(self.dynamic, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox3.Add(self.trajectory, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - boxsizer.Add(self.hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(self.hbox2, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - config_file = auxiliaryfunctions.read_config(self.config) - if config_file.get("multianimalproject", False): - bodyparts = config_file["multianimalbodyparts"] - else: - bodyparts = config_file["bodyparts"] - self.trajectory_to_plot = wx.CheckListBox( - self, choices=bodyparts, style=0, name="Select the bodyparts" - ) - self.trajectory_to_plot.Bind(wx.EVT_CHECKLISTBOX, self.getbp) - self.trajectory_to_plot.SetCheckedItems(range(len(bodyparts))) - self.trajectory_to_plot.Hide() - - self.draw_skeleton = wx.RadioBox( - self, - label="Include the skeleton in the video?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.draw_skeleton.Bind(wx.EVT_RADIOBOX, self.choose_draw_skeleton_options) - self.draw_skeleton.SetSelection(1) - self.draw_skeleton.Hide() - - self.trail_points_text = wx.StaticBox( - self, label="Specify the number of trail points" - ) - trail_pointsboxsizer = wx.StaticBoxSizer(self.trail_points_text, wx.VERTICAL) - self.trail_points = wx.SpinCtrl(self, value="1") - trail_pointsboxsizer.Add( - self.trail_points, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - self.trail_points_text.Hide() - self.trail_points.Hide() - - self.hbox3.Add(self.trajectory_to_plot, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - boxsizer.Add(self.hbox3, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.hbox4.Add(self.draw_skeleton, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox4.Add(trail_pointsboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - boxsizer.Add(self.hbox4, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - self.sizer.Add( - boxsizer, - pos=(5, 0), - span=(1, 10), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(7, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="Step 1: Analyze Videos") - self.sizer.Add(self.ok, pos=(7, 4), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.ok.Bind(wx.EVT_BUTTON, self.analyze_videos) - - if config_file.get("multianimalproject", False): - self.ok = wx.Button(self, label="Step 2: Convert to Tracklets") - self.sizer.Add(self.ok, pos=(7, 5), border=10) - self.ok.Bind(wx.EVT_BUTTON, self.convert2_tracklets) - - self.inf_cfg_text = wx.Button(self, label="Edit inference_config.yaml") - self.sizer.Add(self.inf_cfg_text, pos=(8, 5), border=10) - self.inf_cfg_text.Bind(wx.EVT_BUTTON, self.edit_inf_config) - - self.reset = wx.Button(self, label="Reset") - self.sizer.Add( - self.reset, pos=(7, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_analyze_videos) - - self.edit_config_file = wx.Button(self, label="Edit config.yaml") - self.sizer.Add(self.edit_config_file, pos=(8, 4)) - self.edit_config_file.Bind(wx.EVT_BUTTON, self.edit_config) - - self.sizer.AddGrowableCol(2) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def edit_config(self, event): - """ - """ - if platform.system() == "Darwin": - self.file_open_bool = subprocess.call(["open", self.config]) - self.file_open_bool = True - else: - self.file_open_bool = webbrowser.open(self.config) - if self.file_open_bool: - self.pose_cfg = auxiliaryfunctions.read_config(self.config) - else: - raise FileNotFoundError("File not found!") - - def edit_inf_config(self, event): - # Read the infer config file - cfg = auxiliaryfunctions.read_config(self.config) - trainingsetindex = self.trainingset.GetValue() - trainFraction = cfg["TrainingFraction"][trainingsetindex] - self.inf_cfg_path = os.path.join( - cfg["project_path"], - auxiliaryfunctions.GetModelFolder( - trainFraction, self.shuffle.GetValue(), cfg - ), - "test", - "inference_cfg.yaml", - ) - # let the user open the file with default text editor. Also make it mac compatible - if sys.platform == "darwin": - self.file_open_bool = subprocess.call(["open", self.inf_cfg_path]) - self.file_open_bool = True - else: - self.file_open_bool = webbrowser.open(self.inf_cfg_path) - if self.file_open_bool: - self.inf_cfg = auxiliaryfunctions.read_config(self.inf_cfg_path) - else: - raise FileNotFoundError("File not found!") - - def activate_change_wd(self, event): - """ - Activates the option to change the working directory - """ - self.change_wd = event.GetEventObject() - if self.change_wd.GetValue(): - self.sel_wd.Enable(True) - else: - self.sel_wd.Enable(False) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.analyze_videos" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def convert2_tracklets(self, event): - shuffle = self.shuffle.GetValue() - trainingsetindex = self.trainingset.GetValue() - if self.overwrite.GetStringSelection() == "Yes": - overwrite = True - else: - overwrite = False - deeplabcut.convert_detections2tracklets( - self.config, - self.filelist, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - overwrite=overwrite, - track_method=self.trackertypes.GetValue(), - calibrate=self.calibrate.GetStringSelection() == "Yes", - window_size=self.winsize.GetValue(), - ) - - # def video_tracklets(self,event): - # shuffle = self.shuffle.GetValue() - # trainingsetindex = self.trainingset.GetValue() - # deeplabcut.create_video_from_pickled_tracks(self.filelist, picklefile, pcutoff=0.6) - - def select_videos(self, event): - """ - Selects the videos from the directory - """ - cwd = os.getcwd() - dlg = wx.FileDialog( - self, "Select videos to analyze", cwd, "", "*.*", wx.FD_MULTIPLE - ) - if dlg.ShowModal() == wx.ID_OK: - self.vids = dlg.GetPaths() - self.filelist = self.filelist + self.vids - self.sel_vids.SetLabel("Total %s Videos selected" % len(self.filelist)) - - def choose_draw_skeleton_options(self, event): - if self.draw_skeleton.GetStringSelection() == "Yes": - self.draw = True - else: - self.draw = False - - def analyze_videos(self, event): - - shuffle = self.shuffle.GetValue() - trainingsetindex = self.trainingset.GetValue() - - if self.cfg.get("multianimalproject", False): - print("Analyzing ... ") - else: - if self.csv.GetStringSelection() == "Yes": - save_as_csv = True - else: - save_as_csv = False - if self.dynamic.GetStringSelection() == "No": - dynamic = (False, 0.5, 10) - else: - dynamic = (True, 0.5, 10) - if self.filter.GetStringSelection() == "No": - _filter = False - else: - _filter = True - - if self.cfg["cropping"] == "True": - crop = self.cfg["x1"], self.cfg["x2"], self.cfg["y1"], self.cfg["y2"] - else: - crop = None - - if self.cfg.get("multianimalproject", False): - if self.robust.GetStringSelection() == "No": - robust = False - else: - robust = True - scorername = deeplabcut.analyze_videos( - self.config, - self.filelist, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - gputouse=None, - cropping=crop, - robust_nframes=robust, - ) - if self.create_video_with_all_detections.GetStringSelection() == "Yes": - deeplabcut.create_video_with_all_detections( - self.config, self.filelist, shuffle, trainingsetindex - ) - else: - scorername = deeplabcut.analyze_videos( - self.config, - self.filelist, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - gputouse=None, - save_as_csv=save_as_csv, - cropping=crop, - dynamic=dynamic, - ) - if _filter: - deeplabcut.filterpredictions( - self.config, - self.filelist, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - filtertype="median", - windowlength=5, - save_as_csv=save_as_csv, - ) - - if self.trajectory.GetStringSelection() == "Yes": - if self.showfigs.GetStringSelection() == "No": - showfig = False - else: - showfig = True - deeplabcut.plot_trajectories( - self.config, - self.filelist, - displayedbodyparts=self.bodyparts, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - filtered=_filter, - showfigures=showfig, - ) - - def reset_analyze_videos(self, event): - """ - Reset to default - """ - if self.cfg.get("multianimalproject", False): - self.create_video_with_all_detections.SetSelection(1) - else: - self.csv.SetSelection(1) - self.filter.SetSelection(1) - self.trajectory.SetSelection(1) - self.dynamic.SetSelection(1) - # self.select_destfolder.SetPath("None") - self.config = [] - self.sel_config.SetPath("") - self.videotype.SetStringSelection(".avi") - self.sel_vids.SetLabel("Select videos to analyze") - self.filelist = [] - self.shuffle.SetValue(1) - self.trainingset.SetValue(0) - if self.draw_skeleton.IsShown(): - self.draw_skeleton.SetSelection(1) - self.draw_skeleton.Hide() - self.trail_points_text.Hide() - self.trail_points.Hide() - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def chooseOption(self, event): - if self.trajectory.GetStringSelection() == "Yes": - self.trajectory_to_plot.Show() - self.getbp(event) - if self.trajectory.GetStringSelection() == "No": - self.trajectory_to_plot.Hide() - self.bodyparts = [] - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def getbp(self, event): - self.bodyparts = list(self.trajectory_to_plot.GetCheckedStrings()) diff --git a/deeplabcut/gui/assets/icons/help.png b/deeplabcut/gui/assets/icons/help.png new file mode 100644 index 0000000000..221ea6629a Binary files /dev/null and b/deeplabcut/gui/assets/icons/help.png differ diff --git a/deeplabcut/gui/assets/icons/help2.png b/deeplabcut/gui/assets/icons/help2.png new file mode 100644 index 0000000000..43b7099ee9 Binary files /dev/null and b/deeplabcut/gui/assets/icons/help2.png differ diff --git a/deeplabcut/gui/assets/icons/new_project.png b/deeplabcut/gui/assets/icons/new_project.png new file mode 100644 index 0000000000..447f015de7 Binary files /dev/null and b/deeplabcut/gui/assets/icons/new_project.png differ diff --git a/deeplabcut/gui/assets/icons/new_project2.png b/deeplabcut/gui/assets/icons/new_project2.png new file mode 100644 index 0000000000..7e82b3dad5 Binary files /dev/null and b/deeplabcut/gui/assets/icons/new_project2.png differ diff --git a/deeplabcut/gui/assets/icons/open.png b/deeplabcut/gui/assets/icons/open.png new file mode 100644 index 0000000000..91e62d425e Binary files /dev/null and b/deeplabcut/gui/assets/icons/open.png differ diff --git a/deeplabcut/gui/assets/icons/open2.png b/deeplabcut/gui/assets/icons/open2.png new file mode 100644 index 0000000000..2b61c16023 Binary files /dev/null and b/deeplabcut/gui/assets/icons/open2.png differ diff --git a/deeplabcut/gui/assets/logo.png b/deeplabcut/gui/assets/logo.png new file mode 100644 index 0000000000..ec77b4a720 Binary files /dev/null and b/deeplabcut/gui/assets/logo.png differ diff --git a/deeplabcut/gui/assets/logo_transparent.png b/deeplabcut/gui/assets/logo_transparent.png new file mode 100644 index 0000000000..45dd2a38ae Binary files /dev/null and b/deeplabcut/gui/assets/logo_transparent.png differ diff --git a/deeplabcut/gui/assets/style.qss b/deeplabcut/gui/assets/style.qss new file mode 100644 index 0000000000..feaf66e3d4 --- /dev/null +++ b/deeplabcut/gui/assets/style.qss @@ -0,0 +1,31 @@ + /* + Variables used + -------------- + + widgets height: 25px + + */ + +QPushButton{ + height: 25px; + min-width: 100px; +} + +QSpinBox{ + height: 25px; + width: 100px +} + +QDoubleSpinBox{ + height: 25px; + width: 100px +} + +QComboBox{ + height: 25px; + min-width: 100px; +} + +QLineEdit{ + height: 25px; +} diff --git a/deeplabcut/gui/assets/welcome.png b/deeplabcut/gui/assets/welcome.png new file mode 100644 index 0000000000..9afebe0f41 Binary files /dev/null and b/deeplabcut/gui/assets/welcome.png differ diff --git a/deeplabcut/gui/auxfun_drag.py b/deeplabcut/gui/auxfun_drag.py deleted file mode 100644 index f808276435..0000000000 --- a/deeplabcut/gui/auxfun_drag.py +++ /dev/null @@ -1,173 +0,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 -""" - -""" -Class to drag,annotate and remove the data points. Use left click to drag the data points -around. Use right click to remove any unwanted data points. Internally the corresponding data -point is set to nan. When the user hovers the mouse over any data point, each data point is annotated with the labels and its corresponding likelihood. These annotations also move with the drag! -""" - - -import numpy as np -import wx - - -class DraggablePoint: - lock = None # only one can be animated at a time - - def __init__(self, point, bodyParts, individual_names=None, likelihood=None): - self.point = point - self.bodyParts = bodyParts - self.individual_names = individual_names - self.likelihood = likelihood - self.press = None - self.background = None - self.final_point = (0.0, 0.0) - self.annot = self.point.axes.annotate( - "", - xy=(0, 0), - xytext=(20, 20), - textcoords="offset points", - bbox=dict(boxstyle="round", fc="w"), - arrowprops=dict(arrowstyle="->"), - ) - self.annot.set_visible(False) - self.coords = [] - - 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 - ) - - def on_press(self, event): - """ - Define the event for the button press! - """ - if event.inaxes != self.point.axes: - return - if DraggablePoint.lock is not None: - return - contains, attrd = self.point.contains(event) - if not contains: - return - if event.button == 1: - """ - 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 - axes = self.point.axes - self.point.set_animated(True) - canvas.draw() - self.background = canvas.copy_from_bbox(self.point.axes.bbox) - 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 right 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: - message += " You cannot undo this step!" - msg = wx.MessageBox(message, "Remove!", wx.YES_NO | wx.ICON_WARNING) - if msg == 2: - self.delete_data() - - def delete_data(self): - self.press = None - DraggablePoint.lock = None - self.point.set_animated(False) - self.background = None - self.final_point = (np.nan, np.nan, self.individual_names, self.bodyParts) - self.point.center = (np.nan, np.nan) - self.coords.append(self.final_point) - self.point.figure.canvas.draw() - - def on_motion(self, event): - """ - During the drag! - """ - if DraggablePoint.lock is not self: - return - if event.inaxes != self.point.axes: - return - - if event.button == 1: - self.point.center, xpress, ypress = self.press - dx = event.xdata - xpress - dy = event.ydata - ypress - self.point.center = (self.point.center[0] + dx, self.point.center[1] + dy) - canvas = self.point.figure.canvas - axes = self.point.axes - # restore the background region - canvas.restore_region(self.background) - axes.draw_artist(self.point) - canvas.blit(axes.bbox) - - def on_release(self, event): - "on release we reset the press data" - if DraggablePoint.lock is not self: - return - if event.button == 1: - self.press = None - DraggablePoint.lock = None - self.point.set_animated(False) - self.background = None - self.point.figure.canvas.draw() - self.final_point = ( - self.point.center[0], - self.point.center[1], - self.individual_names, - self.bodyParts, - ) - self.coords.append(self.final_point) - - def on_hover(self, event): - """ - Annotate the lables and likelihood when the user hovers over the data points. - """ - vis = self.annot.get_visible() - - if event.inaxes == self.point.axes: - contains, attrd = self.point.contains(event) - if contains: - self.annot.xy = (self.point.center[0], self.point.center[1]) - text = str(self.bodyParts) - if self.individual_names is not None: - text = f"{self.individual_names},{text}" - if self.likelihood is not None: - text += f",p={self.likelihood:.2f}" - self.annot.set_text(text) - self.annot.get_bbox_patch().set_alpha(0.4) - self.annot.set_visible(True) - self.point.figure.canvas.draw_idle() - else: - if vis: - self.annot.set_visible(False) - - def disconnect(self): - "disconnect all the stored connection ids" - self.point.figure.canvas.mpl_disconnect(self.cidpress) - self.point.figure.canvas.mpl_disconnect(self.cidrelease) - self.point.figure.canvas.mpl_disconnect(self.cidmotion) - self.point.figure.canvas.mpl_disconnect(self.cidhover) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py new file mode 100644 index 0000000000..06d1fedc58 --- /dev/null +++ b/deeplabcut/gui/components.py @@ -0,0 +1,794 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt, Slot + +from deeplabcut.core.config import read_config_as_dict +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.gui_assets import icon_from_resource +from deeplabcut.gui.widgets import ConfigEditor + +PathInput = str | Path +Margins = tuple[int, int, int, int] + + +def _create_label_widget( + text: str, + style: str = "", + margins: Margins = (20, 10, 0, 10), +) -> QtWidgets.QLabel: + label = QtWidgets.QLabel(text) + label.setContentsMargins(*margins) + label.setStyleSheet(style) + return label + + +def _create_horizontal_layout( + alignment: Qt.AlignmentFlag | None = None, + spacing: int = 20, + margins: Margins = (20, 0, 0, 0), +) -> QtWidgets.QHBoxLayout: + layout = QtWidgets.QHBoxLayout() + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) + layout.setSpacing(spacing) + layout.setContentsMargins(*margins) + return layout + + +def _create_vertical_layout( + alignment: Qt.AlignmentFlag | None = None, + spacing: int = 20, + margins: Margins = (20, 0, 0, 0), +) -> QtWidgets.QVBoxLayout: + layout = QtWidgets.QVBoxLayout() + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) + layout.setSpacing(spacing) + layout.setContentsMargins(*margins) + return layout + + +def _create_grid_layout( + alignment: Qt.AlignmentFlag | None = None, + spacing: int = 20, + margins: Margins | None = None, +) -> QtWidgets.QGridLayout: + layout = QtWidgets.QGridLayout() + layout.setAlignment(alignment if alignment is not None else Qt.AlignLeft | Qt.AlignTop) + layout.setSpacing(spacing) + + if margins is not None: + layout.setContentsMargins(*margins) + + return layout + + +def _dialog_directory(directory: PathInput | None) -> str: + """Convert an optional path to a QFileDialog-compatible string.""" + if directory is None or directory == "": + return "" + return str(directory) + + +def _get_open_file_name( + parent: QtWidgets.QWidget, + caption: str, + directory: PathInput | None, + file_filter: str, +) -> tuple[str, str]: + """Open a binding-compatible single-file dialog.""" + return QtWidgets.QFileDialog.getOpenFileName( + parent, + caption, + _dialog_directory(directory), + file_filter, + ) + + +def _get_open_file_names( + parent: QtWidgets.QWidget, + caption: str, + directory: PathInput | None, + file_filter: str, +) -> tuple[list[str], str]: + """Open a binding-compatible multiple-file dialog.""" + return QtWidgets.QFileDialog.getOpenFileNames( + parent, + caption, + _dialog_directory(directory), + file_filter, + ) + + +def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int = 0) -> None: + """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 + depending on the underlying Qt model and signal connections. + + It also handles the edge case where the item at the target index is already + selected: by default, Qt will not emit a signal if the index doesn't change. + To ensure consistent behavior, this method temporarily sets the index to -1 + (i.e., no selection), which is done with signals blocked, then restores the + intended index — causing the signal to emit once and only once. + + Parameters: + combo_box (QComboBox): The combo box to update. + items (list of str): New items to populate the combo box. + index (int): The index to select after updating items. Defaults to 0. + + Note: + - If the items list is empty, no item will be selected and no signal will be emitted. + - This method is designed to be safe for use with PySide, where signals + cannot be manually emitted, and future-proof if multiple slots are connected. + """ + + previous = combo_box.blockSignals(True) + try: + combo_box.clear() + combo_box.addItems(items) + + if not items: + combo_box.setCurrentIndex(-1) + return + + if combo_box.currentIndex() == index: + combo_box.setCurrentIndex(-1) + finally: + combo_box.blockSignals(previous) + + combo_box.setCurrentIndex(index) + + +class BodypartListWidget(QtWidgets.QListWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + # all_bodyparts: List + # NOTE: Is there a case where a specific list should + # have bodyparts other than the root? I don't think so. + ): + super().__init__() + + self.root = root + self.parent = parent + self.selected_bodyparts = self.root.all_bodyparts + + self.setEnabled(False) + self.setMaximumWidth(600) + self.setMaximumHeight(500) + self.hide() + + self.addItems(self.root.all_bodyparts) + self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + + self.itemSelectionChanged.connect(self.update_selected_bodyparts) + + def refresh(self): + self.clear() + self.addItems(self.root.all_bodyparts) + self.update_selected_bodyparts() + + def update_selected_bodyparts(self): + self.selected_bodyparts = [item.text() for item in self.selectedItems()] + self.root.logger.info(f"Selected bodyparts:\n\t{self.selected_bodyparts}") + + +class VideoSelectionWidget(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + *, + hide_videotype: bool = False, + sync_videotype_with_selection: bool = False, + strict_videotype_filter: bool = False, + ): + super().__init__(parent) + + self.root = root + self.parent = parent + + # Optional safeties; defaults preserve current behavior + self.sync_videotype_with_selection = sync_videotype_with_selection + self.strict_videotype_filter = strict_videotype_filter + + self._init_layout(hide_videotype) + + def _init_layout(self, hide_videotype: bool): + layout = _create_horizontal_layout() + + # Videotype selection + self.videotype_widget = QtWidgets.QComboBox() + self.videotype_widget.setMinimumWidth(100) + self.videotype_widget.addItems(DLCParams.VIDEOTYPES) + self.videotype_widget.setCurrentText(self._normalize_videotype(self.root.video_type)) + self.root.video_type_.connect(self._sync_videotype_from_root) + self.videotype_widget.currentTextChanged.connect(self.update_videotype) + + # Select videos + self.select_video_button = QtWidgets.QPushButton("Select videos") + self.select_video_button.setMaximumWidth(200) + self.select_video_button.clicked.connect(self.update_videos) + self.root.video_files_.connect(self._update_video_selection) + + # Number of selected videos text + self.selected_videos_text = QtWidgets.QLabel("") + self.selected_videos_text.setWordWrap(True) + self.selected_videos_text.setSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Preferred, + ) + + # Clear video selection + self.clear_videos = QtWidgets.QPushButton("Clear selection") + self.clear_videos.clicked.connect(self.clear_selected_videos) + + if not hide_videotype: + layout.addWidget(self.videotype_widget) + layout.addWidget(self.select_video_button) + layout.addWidget(self.selected_videos_text) + layout.addWidget(self.clear_videos, alignment=Qt.AlignRight) + + self.setLayout(layout) + + @property + def files(self): + return self.root.video_files + + def _normalize_videotype(self, vtype: str) -> str: + return (vtype or "").lower().lstrip(".") + + @property + def selected_suffixes(self) -> set[str]: + """Return normalized suffixes (without leading dot) of currently selected files.""" + return {Path(video).suffix.lower().lstrip(".") for video in self.files if Path(video).suffix} + + @Slot(str) + def _sync_videotype_from_root(self, vtype: str) -> None: + normalized = self._normalize_videotype(vtype) + + if normalized == self._normalize_videotype(self.videotype_widget.currentText()): + return + + previous = self.videotype_widget.blockSignals(True) + try: + self.videotype_widget.setCurrentText(normalized) + finally: + self.videotype_widget.blockSignals(previous) + + def get_effective_videotype( + self, + prefer_selected_files: bool = False, + with_dot: bool = True, + ) -> str: + """ + Return the videotype to use. + + By default, preserves current behavior and uses the dropdown. + If prefer_selected_files=True and the selected files all share one suffix, + that suffix is used instead. + """ + videotype = self._normalize_videotype(self.videotype_widget.currentText()) + + if prefer_selected_files: + suffixes = self.selected_suffixes + if len(suffixes) == 1: + videotype = next(iter(suffixes)) + + if with_dot and videotype: + return f".{videotype}" + return videotype + + def get_files_grouped_by_suffix( + self, + keep_dot: bool = False, + ) -> dict[str, list[Path]]: + """Return selected files grouped by suffix.""" + groups: dict[str, list[Path]] = {} + + for video in self.files: + path = Path(video) + suffix = path.suffix.lower() + + if not keep_dot: + suffix = suffix.lstrip(".") + + groups.setdefault(suffix, []).append(path) + + return groups + + def _all_supported_video_patterns(self) -> list[str]: + """Return all supported video patterns in both lower and upper case.""" + return [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [ + f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:] + ] + + def _build_video_filter(self) -> str: + """ + Build the file dialog filter. + + By default, preserve current behavior: show all supported video types. + If strict_videotype_filter is enabled, restrict to the currently selected + videotype when it is non-empty. If the current dropdown value is empty + (the "all types" option), fall back to the full supported-extension filter. + """ + all_video_types = self._all_supported_video_patterns() + + if self.strict_videotype_filter: + current = self.get_effective_videotype( + prefer_selected_files=False, + with_dot=False, + ) + + if current: + video_types = [f"*.{current.lower()}", f"*.{current.upper()}"] + else: + # "All types" entry selected: keep the dialog usable + video_types = all_video_types + else: + video_types = all_video_types + + return f"Videos ({' '.join(video_types)})" + + def _set_videotype_silently(self, vtype: str) -> None: + normalized = self._normalize_videotype(vtype) + current = self._normalize_videotype(self.videotype_widget.currentText()) + + if not normalized: + self.root.logger.warning("Attempted to set an empty videotype silently; keeping current selection.") + return + + if self.videotype_widget.findText(normalized) == -1: + self.root.logger.warning( + f"Attempted to set unsupported videotype " + f"{normalized!r} silently; keeping current videotype " + f"{current!r}." + ) + return + + previous = self.videotype_widget.blockSignals(True) + try: + self.videotype_widget.setCurrentText(normalized) + finally: + self.videotype_widget.blockSignals(previous) + + if self._normalize_videotype(self.root.video_type) != normalized: + self.root.video_type = normalized + + @Slot(str) + def update_videotype(self, vtype: str) -> None: + normalized = self._normalize_videotype(vtype) + current = self._normalize_videotype(self.root.video_type) + + if normalized == current: + return + + self.clear_selected_videos() + self.root.video_type = normalized + + def _update_video_selection(self, _videopaths) -> None: + n_videos = len(self.root.video_files) + + if not n_videos: + self.selected_videos_text.setText("") + self.select_video_button.setText("Select videos") + return + + suffixes = self.selected_suffixes + + if len(suffixes) == 1: + suffix = next(iter(suffixes)) + text = f"{n_videos} videos selected (.{suffix})" + elif len(suffixes) > 1: + counts = {suffix: len(files) for suffix, files in self.get_files_grouped_by_suffix().items()} + summary = ", ".join(f"{count} .{suffix}" for suffix, count in sorted(counts.items())) + text = f"{n_videos} videos selected ({summary}; will run in separate batches)" + else: + text = f"{n_videos} videos selected" + + self.selected_videos_text.setText(text) + self.select_video_button.setText("Add more videos") + + def update_videos(self): + video_filter = self._build_video_filter() + + filenames, _ = _get_open_file_names( + self, + "Select video(s) to analyze", + self.root.project_folder, + video_filter, + ) + + if not filenames: + return + + abs_files = [Path(filename).absolute() for filename in filenames] + self.root.add_video_files(abs_files) + + if not self.sync_videotype_with_selection: + return + + suffixes = {video.suffix.lower().lstrip(".") for video in abs_files if video.suffix} + + if len(suffixes) == 1: + inferred = next(iter(suffixes)) + self._set_videotype_silently(inferred) + self.root.logger.info(f"Inferred videotype {inferred!r} from selected file(s)") + elif len(suffixes) > 1: + self.root.logger.warning( + f"Selected videos have mixed suffixes {sorted(suffixes)}; keeping current videotype dropdown unchanged." + ) + + def clear_selected_videos(self): + self.root.clear_video_files() + self.root.logger.debug("Cleared selected videos") + + +class SnapshotSelectionWidget(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + margins: tuple, + select_button_text: str, + ): + super().__init__(parent) + self.root = root + self.parent = parent + self.selected_snapshot: Path | None = None + self._init_layout(margins, select_button_text) + + def _init_layout(self, margins, select_button_text): + layout = _create_horizontal_layout(margins=margins) + + # Select snapshot + self.select_snapshot_button = QtWidgets.QPushButton(select_button_text) + self.select_snapshot_button.setMaximumWidth(200) + self.select_snapshot_button.clicked.connect(self.select_snapshot) + + # Selected snapshot text + self.selected_snapshot_text = QtWidgets.QLabel("") # updated when snapshot is selected + + # Clear snapshot selection + self.clear_snapshot_button = QtWidgets.QPushButton("Clear selection") + self.clear_snapshot_button.clicked.connect(self.clear_selected_snapshot) + self.clear_snapshot_button.hide() + + layout.addWidget(self.select_snapshot_button) + layout.addWidget(self.selected_snapshot_text) + layout.addWidget(self.clear_snapshot_button, alignment=Qt.AlignRight) + + self.setLayout(layout) + + def _update_selected_snapshot_display(self): + if self.selected_snapshot is None: + self.selected_snapshot_text.setText("") + self.clear_snapshot_button.hide() + else: + self.selected_snapshot_text.setText(self.selected_snapshot.name) + self.clear_snapshot_button.show() + + def select_snapshot(self): + snapshot_types = ["*.pt", "*.PT"] + snapshot_filter = f"Snapshots ({' '.join(snapshot_types)})" + + selected_snapshot, _ = _get_open_file_name( + self, + "Select snapshot to start training from", + self.root.models_folder, + snapshot_filter, + ) + + if selected_snapshot: + self.selected_snapshot = Path(selected_snapshot).absolute() + + self._update_selected_snapshot_display() + + def clear_selected_snapshot(self): + self.selected_snapshot = None + self._update_selected_snapshot_display() + + +class ConditionsSelectionWidget(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget, + ): + super().__init__(parent=parent) + self.root = root + self.parent = parent + self.selected_conditions: Path | None = None + self._init_layout() + + def _init_layout(self): + layout = _create_horizontal_layout() + + # Select conditions + self.select_conditions_button = QtWidgets.QPushButton("Select conditions") + self.select_conditions_button.setMaximumWidth(200) + self.select_conditions_button.clicked.connect(self.select_conditions) + + # Selected conditions text + self.selected_conditions_text = QtWidgets.QLabel("") # updated when conditions are selected + + layout.addWidget(self.select_conditions_button) + layout.addWidget(self.selected_conditions_text) + + self.setLayout(layout) + + def _update_selected_conditions_display(self): + def _shorten_path(path: Path | str, max_length: int = 30) -> str: + path_str = str(path) + if len(path_str) <= max_length: + return path_str + return "..." + path_str[-(max_length - 3) :] + + self.selected_conditions_text.setText( + "" if self.selected_conditions is None else _shorten_path(self.selected_conditions) + ) + + def select_conditions(self): + def _is_model_bu( + selected_conditions: PathInput, + ) -> bool: + model_config_path = Path(selected_conditions).parent / "pytorch_config.yaml" + model_config = read_config_as_dict(model_config_path) + method = model_config.get("method") + + return isinstance(method, str) and method.lower() == "bu" + + # Create a filter string with both lowercase and uppercase extensions + snapshots_label = "Snapshots" + h5_predictions_label = "H5 predictions" + json_prediction_label = "Json predictions" + snapshot_types = ["*.pt", "*.PT"] + h5_predictions_types = ["*.h5", "*.H5"] + json_prediction_types = ["*.json", "*.JSON"] + conditions_filter = ";;".join( + [ + f"{snapshots_label} ({' '.join(snapshot_types)})", + f"{h5_predictions_label} ({' '.join(h5_predictions_types)})", + f"{json_prediction_label} ({' '.join(json_prediction_types)})", + ] + ) + + selected_conditions, selected_filter = _get_open_file_name( + self, + ("Select conditions to use during inference (snapshot or predictions file)"), + self.root.project_folder, + conditions_filter, + ) + + if ( + selected_conditions + and selected_filter.startswith(snapshots_label) + and not _is_model_bu(selected_conditions) + ): + msg = _create_message_box( + "Invalid conditions", + ( + f"The selected snapshot " + f"({selected_conditions}) cannot be used " + "as conditions because it is not a " + "Bottom-Up model." + ), + ) + msg.exec() + selected_conditions = None + + self.selected_conditions = Path(selected_conditions).absolute() if selected_conditions else None + + self._update_selected_conditions_display() + + +class TrainingSetSpinBox(QtWidgets.QSpinBox): + def __init__(self, root, parent): + super().__init__(parent) + + self.root = root + self.parent = parent + + self.setMaximum(100) + self.setValue(self.root.trainingset_index) + self.valueChanged.connect(self.root.update_trainingset) + + +class ShuffleSpinBox(QtWidgets.QSpinBox): + def __init__(self, root, parent): + super().__init__(parent) + + self.root = root + self.parent = parent + + self.setMaximum(10_000) + self.setValue(self.root.shuffle_value) + self.valueChanged.connect(self.root.update_shuffle) + self.root.shuffle_change.connect(self.update_shuffle) + + @Slot(int) + def update_shuffle(self, new_shuffle: int) -> None: + if new_shuffle == self.value(): + return + + previous = self.blockSignals(True) + try: + self.setValue(new_shuffle) + finally: + self.blockSignals(previous) + + +class DefaultTab(QtWidgets.QWidget): + def __init__( + self, + root: QtWidgets.QMainWindow, + parent: QtWidgets.QWidget | None = None, + h1_description: str = "", + ): + super().__init__(parent) + + self.parent = parent + self.root = root + self.h1_description = h1_description + + outer_layout = QtWidgets.QVBoxLayout(self) + outer_layout.setContentsMargins(0, 0, 0, 0) + + self.scroll_area = QtWidgets.QScrollArea(self) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + + self.content_widget = QtWidgets.QWidget() + self.content_widget.setSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Preferred, + ) + + self.main_layout = QtWidgets.QVBoxLayout(self.content_widget) + self.main_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + + self.scroll_area.setWidget(self.content_widget) + outer_layout.addWidget(self.scroll_area) + + self._init_default_layout() + + def _init_default_layout(self): + # Add tab header + self.main_layout.addWidget(_create_label_widget(self.h1_description, "font:bold;", (10, 10, 0, 10))) + + # Add separating line + self.separator = QtWidgets.QFrame() + self.separator.setFrameShape(QtWidgets.QFrame.HLine) + self.separator.setFrameShadow(QtWidgets.QFrame.Raised) + self.separator.setLineWidth(0) + self.separator.setMidLineWidth(1) + policy = QtWidgets.QSizePolicy() + policy.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Fixed) + policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding) + self.separator.setSizePolicy(policy) + self.main_layout.addWidget(self.separator) + + +class EditYamlButton(QtWidgets.QPushButton): + def __init__(self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None): + super().__init__(button_label, parent) + self.filepath = filepath + self.parent = parent + self._editor: ConfigEditor | None = None + + self.clicked.connect(self.open_config) + + def open_config(self): + self._editor = ConfigEditor(self.filepath) + self._editor.show() + + +class BrowseFilesButton(QtWidgets.QPushButton): + def __init__( + self, + button_label: str, + filetype: str = None, + cwd: PathInput | None = None, + single_file: bool = False, + dialog_text: str = None, + file_text: str = None, + parent=None, + ): + super().__init__(button_label, parent) + self.filetype = filetype + self.single_file_only = single_file + self.cwd = cwd + self.parent = parent + + self.dialog_text = dialog_text + self.file_text = file_text + + self.files: set[Path] = set() + + self.clicked.connect(self.browse_files) + + def browse_files(self) -> None: + file_ext = "*" + if self.filetype: + file_ext = self.filetype.rsplit(".", 1)[-1] + + dialog_text = self.dialog_text or f"Select .{file_ext} files" + file_text = self.file_text or f"Files (*.{file_ext})" + + if self.single_file_only: + filepath, _ = _get_open_file_name( + self, + dialog_text, + self.cwd, + file_text, + ) + if filepath: + self.files.add(Path(filepath).absolute()) + return + + filepaths, _ = _get_open_file_names( + self, + dialog_text, + self.cwd, + file_text, + ) + self.files.update(Path(filepath).absolute() for filepath in filepaths) + + +def _create_message_box(text, info_text): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setText(text) + msg.setInformativeText(info_text) + + msg.setWindowTitle("Info") + msg.setMinimumWidth(900) + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + return msg + + +def _create_confirmation_box(title, description): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setText(title) + msg.setInformativeText(description) + + msg.setWindowTitle("Confirmation") + msg.setMinimumWidth(900) + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) + msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + return msg + + +def set_layout_contents_visible(layout: QtWidgets.QLayout, visible: bool): + for i in range(layout.count()): + item = layout.itemAt(i) + + # If it's a widget item + widget = item.widget() + if widget is not None: + widget.setVisible(visible) + + # If it's a nested layout + child_layout = item.layout() + if child_layout is not None: + set_layout_contents_visible(child_layout, visible) diff --git a/deeplabcut/gui/config_file_monitor.py b/deeplabcut/gui/config_file_monitor.py new file mode 100644 index 0000000000..7bd2789f99 --- /dev/null +++ b/deeplabcut/gui/config_file_monitor.py @@ -0,0 +1,105 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Watch the active project configuration file for external edits.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +from PySide6 import QtCore, QtWidgets + + +class ConfigFileMonitor(QtCore.QObject): + """Offer an explicit reload when the active config file changes on disk.""" + + def __init__( + self, + status_bar: QtWidgets.QStatusBar, + on_reload: Callable[[], bool], + parent: QtCore.QObject | None = None, + debounce_ms: int = 300, + ) -> None: + super().__init__(parent) + self._path: str | None = None + self._loaded_signature: tuple[int, int] | None = None + + self._watcher = QtCore.QFileSystemWatcher(self) + self._watcher.fileChanged.connect(self._on_file_changed) + self._watcher.directoryChanged.connect(self._on_file_changed) + + self._debounce = QtCore.QTimer(self) + self._debounce.setSingleShot(True) + self._debounce.setInterval(debounce_ms) + self._debounce.timeout.connect(self._check_for_change) + + self._reload_button = QtWidgets.QPushButton("Reload configuration") + self._reload_button.clicked.connect(on_reload) + self._reload_button.hide() + status_bar.addPermanentWidget(self._reload_button) + self._status_bar = status_bar + + def set_path(self, path: str | None) -> None: + """Watch ``path`` and its parent directory, clearing any previous watches.""" + watched_files = self._watcher.files() + if watched_files: + self._watcher.removePaths(watched_files) + watched_dirs = self._watcher.directories() + if watched_dirs: + self._watcher.removePaths(watched_dirs) + + self._path = path + self._loaded_signature = None + self._debounce.stop() + self._reload_button.hide() + + if path is not None: + config_path = Path(path) + if config_path.is_file(): + self._watcher.addPath(path) + # Watch the parent directory so atomic file replacements + # (delete + rename) are also detected. + parent = str(config_path.parent) + if parent and Path(parent).is_dir(): + self._watcher.addPath(parent) + + def mark_current(self) -> None: + """Record that the in-memory config matches the file currently on disk.""" + self._loaded_signature = self._signature() + if self._path is not None and Path(self._path).is_file(): + if self._path not in self._watcher.files(): + self._watcher.addPath(self._path) + self._reload_button.hide() + + def _signature(self) -> tuple[int, int] | None: + if self._path is None: + return None + try: + stat = Path(self._path).stat() + except OSError: + return None + return stat.st_mtime_ns, stat.st_size + + def _on_file_changed(self, _path: str) -> None: + self._debounce.start() + + def _check_for_change(self) -> None: + if self._path is None or self._loaded_signature is None: + return + + current = self._signature() + if current == self._loaded_signature: + return + + # Some editors replace the file, which drops the watch path. + # Watching the parent directory helps detect such replacements. + if current is not None and self._path not in self._watcher.files(): + self._watcher.addPath(self._path) + + self._status_bar.showMessage("Project configuration changed on disk.") + self._reload_button.show() diff --git a/deeplabcut/gui/create_new_project.py b/deeplabcut/gui/create_new_project.py deleted file mode 100644 index 1ec6427590..0000000000 --- a/deeplabcut/gui/create_new_project.py +++ /dev/null @@ -1,469 +0,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 -import platform -import pydoc -import subprocess -import sys -import webbrowser - -import wx - -from deeplabcut.create_project import create_new_project, add_new_videos -from deeplabcut.gui.analyze_videos import Analyze_videos -from deeplabcut.gui.create_training_dataset import Create_training_dataset -from deeplabcut.gui.create_videos import Create_Labeled_Videos -from deeplabcut.gui.evaluate_network import Evaluate_network -from deeplabcut.gui.extract_frames import Extract_frames -from deeplabcut.gui.extract_outlier_frames import Extract_outlier_frames -from deeplabcut.gui.label_frames import Label_frames -from deeplabcut.gui.refine_labels import Refine_labels -from deeplabcut.gui.refine_tracklets import Refine_tracklets -from deeplabcut.gui.train_network import Train_network -from deeplabcut.gui.video_editing import Video_Editing -from deeplabcut.utils import auxiliaryfunctions - - -class Create_new_project(wx.Panel): - def __init__(self, parent, gui_size): - self.gui_size = gui_size - self.parent = parent - h = gui_size[0] - w = gui_size[1] - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER, size=(h, w)) - # variable initilization - self.filelist = [] - self.filelistnew = [] - self.dir = None - self.copy = False - self.cfg = None - self.loaded = False - - # design the panel - self.sizer = wx.GridBagSizer(10, 15) - - text1 = wx.StaticText( - self, label="DeepLabCut - Step 1. Create New Project or Load a Project" - ) - self.sizer.Add(text1, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - - # Add logo of DLC - # icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(logo)) - # self.sizer.Add(icon, pos=(0,10), flag=wx.TOP|wx.RIGHT|wx.ALIGN_RIGHT,border=10) - - line = wx.StaticLine(self) - self.sizer.Add( - line, pos=(1, 0), span=(1, 15), flag=wx.EXPAND | wx.BOTTOM, border=5 - ) - - # Add all the options - self.proj = wx.RadioBox( - self, - label="Please choose an option:", - choices=["Create new project", "Load existing project"], - majorDimension=0, - style=wx.RA_SPECIFY_COLS, - ) - self.sizer.Add(self.proj, pos=(2, 0), span=(1, 15), flag=wx.LEFT, border=5) - self.proj.Bind(wx.EVT_RADIOBOX, self.chooseOption) - - # line = wx.StaticLine(self) - # self.sizer.Add(line, pos=(3, 0), span=(1, 8),flag=wx.EXPAND|wx.BOTTOM, border=10) - - self.proj_name = wx.StaticText(self, label="Name of the Project:") - self.sizer.Add(self.proj_name, pos=(4, 0), flag=wx.LEFT, border=15) - - self.proj_name_txt_box = wx.TextCtrl(self) - self.sizer.Add( - self.proj_name_txt_box, pos=(4, 1), span=(1, 5), flag=wx.TOP | wx.EXPAND - ) - - self.exp = wx.StaticText(self, label="Name of the experimenter:") - self.sizer.Add(self.exp, pos=(5, 0), flag=wx.LEFT | wx.TOP, border=15) - - self.exp_txt_box = wx.TextCtrl(self) - self.sizer.Add( - self.exp_txt_box, pos=(5, 1), span=(1, 5), flag=wx.TOP | wx.EXPAND, border=5 - ) - - self.vids = wx.StaticText(self, label="Choose Videos:") - self.sizer.Add(self.vids, pos=(6, 0), flag=wx.TOP | wx.LEFT, border=10) - - self.sel_vids = wx.Button(self, label="Load Videos") - self.sizer.Add( - self.sel_vids, pos=(6, 1), span=(1, 5), flag=wx.TOP | wx.EXPAND, border=10 - ) - self.sel_vids.Bind(wx.EVT_BUTTON, self.select_videos) - # - sb = wx.StaticBox(self, label="Optional Attributes") - self.boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - hbox2 = wx.BoxSizer(wx.HORIZONTAL) - hbox3 = wx.BoxSizer(wx.HORIZONTAL) - hbox4 = wx.BoxSizer(wx.HORIZONTAL) - - self.change_workingdir = wx.CheckBox( - self, label="Select the directory where project will be created" - ) - hbox2.Add(self.change_workingdir) - hbox2.AddSpacer(20) - self.change_workingdir.Bind(wx.EVT_CHECKBOX, self.activate_change_wd) - self.sel_wd = wx.Button(self, label="Browse") - self.sel_wd.Enable(False) - self.sel_wd.Bind(wx.EVT_BUTTON, self.select_working_dir) - hbox2.Add(self.sel_wd, 0, wx.ALL, -1) - self.boxsizer.Add(hbox2) - - self.copy_choice = wx.CheckBox(self, label="Copy the videos") - self.copy_choice.Bind(wx.EVT_CHECKBOX, self.activate_copy_videos) - hbox3.Add(self.copy_choice) - hbox3.AddSpacer(155) - self.boxsizer.Add(hbox3) - - self.multi_choice = wx.CheckBox(self, label="Is it a multi-animal project?") - # self.multi_choice.Bind(wx.EVT_CHECKBOX,self.activate_copy_videos) - hbox4.Add(self.multi_choice) - self.boxsizer.Add(hbox4) - self.sizer.Add( - self.boxsizer, - pos=(7, 0), - span=(1, 10), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(8, 0), flag=wx.LEFT | wx.EXPAND, border=15) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - - self.sizer.Add( - self.sel_config, pos=(8, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.Bind(wx.EVT_BUTTON, self.create_new_project) - self.sel_config.SetPath("") - # Hide the button as this is not the default option - self.sel_config.Hide() - self.cfg_text.Hide() - - self.sel_vids_new = wx.Button(self, label="Load New Videos") - self.sizer.Add(self.sel_vids_new, pos=(9, 2), flag=wx.TOP | wx.EXPAND, border=5) - self.sel_vids_new.Bind(wx.EVT_BUTTON, self.select_new_videos) - self.sel_vids_new.Enable(False) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(10, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="Ok") - self.sizer.Add(self.ok, pos=(9, 5)) - self.ok.Bind(wx.EVT_BUTTON, self.create_new_project) - - self.edit_config_file = wx.Button(self, label="Edit config file") - self.sizer.Add(self.edit_config_file, pos=(10, 4)) - self.edit_config_file.Bind(wx.EVT_BUTTON, self.edit_config) - self.edit_config_file.Enable(False) - - self.reset = wx.Button(self, label="Reset") - self.sizer.Add(self.reset, pos=(10, 1), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.reset.Bind(wx.EVT_BUTTON, self.reset_project) - self.sizer.AddGrowableCol(2) - - self.addvid = wx.Button(self, label="Add New Videos") - self.sizer.Add(self.addvid, pos=(10, 2)) - self.addvid.Bind(wx.EVT_BUTTON, self.add_videos) - self.addvid.Enable(False) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def select_new_videos(self, event): - """ - Selects the videos from the directory - """ - cwd = os.getcwd() - dlg = wx.FileDialog( - self, "Select new videos to load", cwd, "", "*.*", wx.FD_MULTIPLE - ) - if dlg.ShowModal() == wx.ID_OK: - self.addvids = dlg.GetPaths() - self.filelistnew = self.filelistnew + self.addvids - self.sel_vids_new.SetLabel( - "Total %s Videos selected" % len(self.filelistnew) - ) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.create_new_project" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def chooseOption(self, event): - if self.proj.GetStringSelection() == "Load existing project": - - if self.loaded: - self.sel_config.SetPath(self.cfg) - self.proj_name.Enable(False) - self.proj_name_txt_box.Enable(False) - self.exp.Enable(False) - # self.vids.Enable(False) - self.exp_txt_box.Enable(False) - self.sel_vids.Enable(False) - self.sel_vids_new.Enable(False) - self.change_workingdir.Enable(False) - self.copy_choice.Enable(False) - self.multi_choice.Enable(False) - self.sel_config.Show() - self.cfg_text.Show() - self.addvid.Enable(False) - # self.SetSizer(self.sizer) - # self.sizer.Add(self.sizer, pos=(3, 0), span=(1, 8),flag=wx.EXPAND|wx.BOTTOM, border=15) - self.sizer.Fit(self) - else: - self.proj_name.Enable(True) - self.proj_name_txt_box.Enable(True) - self.exp.Enable(True) - self.exp_txt_box.Enable(True) - self.sel_vids.Enable(True) - self.sel_vids_new.Enable(False) - self.change_workingdir.Enable(True) - self.copy_choice.Enable(True) - self.multi_choice.Enable(True) - if self.sel_config.IsShown(): - self.sel_config.Hide() - self.cfg_text.Hide() - # self.ok.Enable(False) - # else: - # self.ok.Enable(True) - self.addvid.Enable(False) - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def edit_config(self, event): - """ - """ - if self.cfg != "": - # For mac compatibility - if platform.system() == "Darwin": - self.file_open_bool = subprocess.call(["open", self.cfg]) - self.file_open_bool = True - else: - self.file_open_bool = webbrowser.open(self.cfg) - - if self.file_open_bool: - pass - else: - raise FileNotFoundError("File not found!") - - def select_videos(self, event): - """ - Selects the videos from the directory - """ - cwd = os.getcwd() - dlg = wx.FileDialog( - self, "Select videos to add to the project", cwd, "", "*.*", wx.FD_MULTIPLE - ) - if dlg.ShowModal() == wx.ID_OK: - self.vids = dlg.GetPaths() - self.filelist = self.filelist + self.vids - self.sel_vids.SetLabel("Total %s Videos selected" % len(self.filelist)) - - def activate_copy_videos(self, event): - """ - Activates the option to copy videos - """ - self.change_copy = event.GetEventObject() - if self.change_copy.GetValue(): - self.copy = True - else: - self.copy = False - - def activate_change_wd(self, event): - """ - Activates the option to change the working directory - """ - self.change_wd = event.GetEventObject() - if self.change_wd.GetValue(): - self.sel_wd.Enable(True) - else: - self.sel_wd.Enable(False) - - def select_working_dir(self, event): - cwd = os.getcwd() - dlg = wx.DirDialog( - self, - "Choose the directory where your project will be saved:", - cwd, - style=wx.DD_DEFAULT_STYLE, - ) - if dlg.ShowModal() == wx.ID_OK: - self.dir = dlg.GetPath() - - def create_new_project(self, event): - """ - Finally create the new project - """ - if self.sel_config.IsShown(): - self.cfg = self.sel_config.GetPath() - if self.cfg == "": - wx.MessageBox( - "Please choose the config.yaml file to load the project", - "Error", - wx.OK | wx.ICON_ERROR, - ) - self.loaded = False - else: - wx.MessageBox("Project Loaded!", "Info", wx.OK | wx.ICON_INFORMATION) - self.loaded = True - self.sel_vids_new.Enable(True) - self.addvid.Enable(True) - self.edit_config_file.Enable(True) - else: - self.task = self.proj_name_txt_box.GetValue() - self.scorer = self.exp_txt_box.GetValue() - - if self.task != "" and self.scorer != "" and self.filelist != []: - self.cfg = create_new_project( - self.task, - self.scorer, - self.filelist, - self.dir, - copy_videos=self.copy, - multianimal=self.multi_choice.IsChecked(), - ) - else: - wx.MessageBox( - "Some of the enteries are missing.\n\nMake sure that the task and experimenter name are specified and videos are selected!", - "Error", - wx.OK | wx.ICON_ERROR, - ) - self.cfg = False - if self.cfg: - wx.MessageBox( - "New Project Created", "Info", wx.OK | wx.ICON_INFORMATION - ) - self.loaded = True - self.edit_config_file.Enable(True) - - # Remove the pages in case the user goes back to the create new project and creates/load a new project - if self.parent.GetPageCount() > 3: - for i in range(2, self.parent.GetPageCount()): - self.parent.RemovePage(2) - self.parent.Layout() - - # Add all the other pages - if self.loaded: - self.edit_config_file.Enable(True) - cfg = auxiliaryfunctions.read_config(self.cfg) - if self.parent.GetPageCount() < 3: - page3 = Extract_frames(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page3, "Extract frames") - - page4 = Label_frames(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page4, "Label frames") - - page5 = Create_training_dataset(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page5, "Create training dataset") - - page6 = Train_network(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page6, "Train network") - - page7 = Evaluate_network(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page7, "Evaluate network") - - page12 = Video_Editing(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page12, "Video editor") - - page8 = Analyze_videos(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page8, "Analyze videos") - - if cfg.get("multianimalproject", False): - page = Refine_tracklets(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page, "Refine tracklets") - page11 = Create_Labeled_Videos(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page11, "Create videos") - page9 = Extract_outlier_frames(self.parent, self.gui_size, self.cfg) - self.parent.AddPage(page9, "Extract outlier frames") - page10 = Refine_labels(self.parent, self.gui_size, self.cfg, page5) - self.parent.AddPage(page10, "Refine labels") - self.edit_config_file.Enable(True) - - def add_videos(self, event): - print("adding new videos to be able to label ...") - self.cfg = self.sel_config.GetPath() - if len(self.filelistnew) > 0: - self.filelistnew = self.filelistnew + self.addvids - add_new_videos(self.cfg, self.filelistnew) - else: - print("Please select videos to add first. Click 'Load New Videos'...") - - def reset_project(self, event): - self.loaded = False - if self.sel_config.IsShown(): - self.sel_config.SetPath("") - self.proj.SetSelection(0) - self.sel_config.Hide() - self.cfg_text.Hide() - - self.sel_config.SetPath("") - self.proj_name_txt_box.SetValue("") - self.exp_txt_box.SetValue("") - self.filelist = [] - self.sel_vids.SetLabel("Load Videos") - self.dir = os.getcwd() - self.edit_config_file.Enable(False) - self.proj_name.Enable(True) - self.proj_name_txt_box.Enable(True) - self.multi_choice.Enable(True) - self.exp.Enable(True) - self.exp_txt_box.Enable(True) - self.sel_vids.Enable(True) - self.addvid.Enable(False) - self.sel_vids_new.Enable(False) - self.change_workingdir.Enable(True) - self.copy_choice.Enable(True) - - try: - self.change_wd.SetValue(False) - except: - pass - try: - self.change_copy.SetValue(False) - except: - pass - # self.yes.Enable(False) - # self.no.Enable(False) - self.sel_wd.Enable(False) diff --git a/deeplabcut/gui/create_training_dataset.py b/deeplabcut/gui/create_training_dataset.py deleted file mode 100644 index 23714d553e..0000000000 --- a/deeplabcut/gui/create_training_dataset.py +++ /dev/null @@ -1,426 +0,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 -import pydoc -import sys - -import wx - -import deeplabcut -from deeplabcut.gui import LOGO_PATH -from deeplabcut.utils import auxiliaryfunctions - - -class Create_training_dataset(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.method = "automatic" - self.config = cfg - # design the panel - self.sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 4. Create training dataset") - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - self.sizer.Add( - icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5 - ) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - sb = wx.StaticBox(self, label="Optional Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox2 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox3 = wx.BoxSizer(wx.HORIZONTAL) - - config_file = auxiliaryfunctions.read_config(self.config) - - net_text = wx.StaticBox(self, label="Select the network") - netboxsizer = wx.StaticBoxSizer(net_text, wx.VERTICAL) - self.net_choice = wx.ComboBox(self, style=wx.CB_READONLY) - options = [ - "dlcrnet_ms5", - "resnet_50", - "resnet_101", - "resnet_152", - "mobilenet_v2_1.0", - "mobilenet_v2_0.75", - "mobilenet_v2_0.5", - "mobilenet_v2_0.35", - "efficientnet-b0", - "efficientnet-b3", - "efficientnet-b6", - ] - self.net_choice.Set(options) - self.net_choice.SetValue("resnet_50") - netboxsizer.Add(self.net_choice, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - aug_text = wx.StaticBox(self, label="Select the augmentation method") - augboxsizer = wx.StaticBoxSizer(aug_text, wx.VERTICAL) - self.aug_choice = wx.ComboBox(self, style=wx.CB_READONLY) - options = ["default", "tensorpack", "imgaug"] - self.aug_choice.Set(options) - self.aug_choice.SetValue("imgaug") - augboxsizer.Add(self.aug_choice, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.hbox1.Add(netboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox1.Add(augboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - shuffle_text = wx.StaticBox( - self, label="Set a specific shuffle indx (1 network only)" - ) - shuffle_text_boxsizer = wx.StaticBoxSizer(shuffle_text, wx.VERTICAL) - self.shuffle = wx.SpinCtrl(self, value="1", min=1, max=100) - shuffle_text_boxsizer.Add(self.shuffle, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - trainingindex_box = wx.StaticBox(self, label="Specify the trainingset index") - trainingindex_boxsizer = wx.StaticBoxSizer(trainingindex_box, wx.VERTICAL) - self.trainingindex = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingindex_boxsizer.Add( - self.trainingindex, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - self.userfeedback = wx.RadioBox( - self, - label="User feedback (to confirm overwrite train/test split)?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.userfeedback.SetSelection(1) - - if config_file.get("multianimalproject", False): - - self.cropandlabel = wx.RadioBox( - self, - label="Crop and Label Data (Yes is required, set crop values)", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.cropandlabel.Bind(wx.EVT_RADIOBOX, self.input_crop_size) - self.cropandlabel.SetSelection(0) - self.crop_text = wx.StaticBox( - self, label="Crop settings (set to smaller than your input images)" - ) - self.crop_sizer = wx.StaticBoxSizer(self.crop_text, wx.VERTICAL) - self.crop_widgets = [] - for name, val in [ - ("# of crops", "10"), - ("height", "400"), - ("width", "400"), - ]: - temp_sizer = wx.BoxSizer(wx.HORIZONTAL) - label = wx.StaticText(self, label=name) - text = wx.TextCtrl(self, value=val) - self.crop_widgets.append([label, text]) - temp_sizer.Add(label, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - temp_sizer.Add(text, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - self.crop_sizer.Add(temp_sizer) - self.crop_sizer.ShowItems(True) - self.hbox3.Add(self.cropandlabel, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox3.Add(self.crop_sizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.hbox2.Add(shuffle_text_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox2.Add(trainingindex_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.hbox3.Add(self.userfeedback, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - if config_file.get("multianimalproject", False): - - self.model_comparison_choice = "No" - print("currently DLCRNet is only supported in multi-animal mode") - else: - self.model_comparison_choice = wx.RadioBox( - self, - label="Want to compare models?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.model_comparison_choice.Bind(wx.EVT_RADIOBOX, self.chooseOption) - self.model_comparison_choice.SetSelection(1) - - self.shuffles_text = wx.StaticBox( - self, label="Specify the number of shuffles" - ) - self.shuffles_text_boxsizer = wx.StaticBoxSizer( - self.shuffles_text, wx.VERTICAL - ) - self.shuffles = wx.SpinCtrl(self, value="1", min=1, max=100) - self.shuffles_text_boxsizer.Add( - self.shuffles, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - networks = [ - "resnet_50", - "resnet_101", - "resnet_152", - "mobilenet_v2_1.0", - "mobilenet_v2_0.75", - "mobilenet_v2_0.5", - "mobilenet_v2_0.35", - "efficientnet-b0", - "efficientnet-b3", - "efficientnet-b6", - ] - augmentation_methods = ["default", "tensorpack", "imgaug"] - self.network_box = wx.StaticBox(self, label="Select the networks") - self.network_boxsizer = wx.StaticBoxSizer(self.network_box, wx.VERTICAL) - self.networks_to_compare = wx.CheckListBox( - self, choices=networks, style=0, name="Select the networks" - ) - self.networks_to_compare.Bind(wx.EVT_CHECKLISTBOX, self.get_network_names) - self.network_boxsizer.Add( - self.networks_to_compare, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - self.augmentation_box = wx.StaticBox( - self, label="Select the augmentation methods" - ) - self.augmentation_boxsizer = wx.StaticBoxSizer( - self.augmentation_box, wx.VERTICAL - ) - self.augmentation_to_compare = wx.CheckListBox( - self, - choices=augmentation_methods, - style=0, - name="Select the augmentation methods", - ) - self.augmentation_to_compare.Bind( - wx.EVT_CHECKLISTBOX, self.get_augmentation_method_names - ) - self.augmentation_boxsizer.Add( - self.augmentation_to_compare, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - self.hbox3.Add( - self.model_comparison_choice, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5 - ) - self.hbox3.Add( - self.shuffles_text_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5 - ) - self.hbox3.Add(self.network_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox3.Add( - self.augmentation_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5 - ) - - self.shuffles_text.Hide() - self.shuffles.Hide() - self.network_box.Hide() - self.networks_to_compare.Hide() - self.augmentation_box.Hide() - self.augmentation_to_compare.Hide() - - boxsizer.Add(self.hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(self.hbox2, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(self.hbox3, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.sizer.Add( - boxsizer, - pos=(3, 0), - span=(1, 5), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(4, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="Ok") - self.sizer.Add(self.ok, pos=(4, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.create_training_dataset) - - self.reset = wx.Button(self, label="Reset") - self.sizer.Add( - self.reset, pos=(4, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_create_training_dataset) - - self.sizer.AddGrowableCol(2) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - self.Layout() - - def input_crop_size(self, event): - if self.cropandlabel.GetStringSelection() == "No": - self.crop_sizer.ShowItems(False) - else: - self.crop_sizer.ShowItems(True) - self.SetSizer(self.sizer) - - def on_focus(self, event): - pass - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.create_training_dataset" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def chooseOption(self, event): - if self.model_comparison_choice.GetStringSelection() == "Yes": - self.network_box.Show() - self.networks_to_compare.Show() - self.augmentation_box.Show() - self.augmentation_to_compare.Show() - self.shuffles_text.Show() - self.shuffles.Show() - self.net_choice.Enable(False) - self.aug_choice.Enable(False) - self.shuffle.Enable(False) - self.SetSizer(self.sizer) - self.sizer.Fit(self) - self.get_network_names(event) - self.get_augmentation_method_names(event) - else: - self.net_choice.Enable(True) - self.aug_choice.Enable(True) - self.shuffle.Enable(True) - self.shuffles_text.Hide() - self.shuffles.Hide() - self.network_box.Hide() - self.networks_to_compare.Hide() - self.augmentation_box.Hide() - self.augmentation_to_compare.Hide() - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def get_network_names(self, event): - self.net_type = list(self.networks_to_compare.GetCheckedStrings()) - - def get_augmentation_method_names(self, event): - self.aug_type = list(self.augmentation_to_compare.GetCheckedStrings()) - - def create_training_dataset(self, event): - """ - """ - num_shuffles = self.shuffle.GetValue() - config_file = auxiliaryfunctions.read_config(self.config) - trainindex = self.trainingindex.GetValue() - - if self.userfeedback.GetStringSelection() == "Yes": - userfeedback = True - else: - userfeedback = False - - if config_file.get("multianimalproject", False): - if self.cropandlabel.GetStringSelection() == "Yes": - n_crops, height, width = [ - int(text.GetValue()) for _, text in self.crop_widgets - ] - deeplabcut.cropimagesandlabels( - self.config, n_crops, (height, width), userfeedback - ) - else: - random = False - deeplabcut.create_multianimaltraining_dataset( - self.config, - num_shuffles, - Shuffles=[self.shuffle.GetValue()], - net_type=self.net_choice.GetValue(), - ) - else: - if self.model_comparison_choice.GetStringSelection() == "No": - deeplabcut.create_training_dataset( - self.config, - num_shuffles, - Shuffles=[self.shuffle.GetValue()], - userfeedback=userfeedback, - net_type=self.net_choice.GetValue(), - augmenter_type=self.aug_choice.GetValue(), - ) - if self.model_comparison_choice.GetStringSelection() == "Yes": - deeplabcut.create_training_model_comparison( - self.config, - trainindex=trainindex, - num_shuffles=num_shuffles, - userfeedback=userfeedback, - net_types=self.net_type, - augmenter_types=self.aug_type, - ) - - def reset_create_training_dataset(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - # self.shuffles.SetValue("1") - self.net_choice.SetValue("resnet_50") - self.aug_choice.SetValue("default") - self.model_comparison_choice.SetSelection(1) - self.network_box.Hide() - self.networks_to_compare.Hide() - self.augmentation_box.Hide() - self.augmentation_to_compare.Hide() - self.shuffles_text.Hide() - self.shuffles.Hide() - self.net_choice.Enable(True) - self.aug_choice.Enable(True) - self.SetSizer(self.sizer) - self.sizer.Fit(self) - self.Layout() diff --git a/deeplabcut/gui/create_videos.py b/deeplabcut/gui/create_videos.py deleted file mode 100644 index 7154c6bd29..0000000000 --- a/deeplabcut/gui/create_videos.py +++ /dev/null @@ -1,408 +0,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 -import pydoc -import sys - -import wx - -import deeplabcut - -from deeplabcut.gui import LOGO_PATH -from deeplabcut.utils import auxiliaryfunctions, skeleton - - -class Create_Labeled_Videos(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - # variable initilization - self.filelist = [] - self.config = cfg - self.bodyparts = [] - self.draw = False - self.slow = False - # design the panel - self.sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Create Labeled Videos") - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - self.sizer.Add( - icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5 - ) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - self.vids = wx.StaticText(self, label="Choose the videos") - self.sizer.Add(self.vids, pos=(3, 0), flag=wx.TOP | wx.LEFT, border=10) - - self.sel_vids = wx.Button(self, label="Select videos") - self.sizer.Add(self.sel_vids, pos=(3, 1), flag=wx.TOP | wx.EXPAND, border=10) - self.sel_vids.Bind(wx.EVT_BUTTON, self.select_videos) - - sb = wx.StaticBox(self, label="Additional Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - hbox1 = wx.BoxSizer(wx.HORIZONTAL) - hbox2 = wx.BoxSizer(wx.HORIZONTAL) - hbox3 = wx.BoxSizer(wx.HORIZONTAL) - hbox4 = wx.BoxSizer(wx.HORIZONTAL) - - videotype_text = wx.StaticBox(self, label="Specify the videotype") - videotype_text_boxsizer = wx.StaticBoxSizer(videotype_text, wx.VERTICAL) - - videotypes = [".avi", ".mp4", ".mov"] - self.videotype = wx.ComboBox(self, choices=videotypes, style=wx.CB_READONLY) - self.videotype.SetValue(".avi") - videotype_text_boxsizer.Add( - self.videotype, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 1 - ) - - shuffle_text = wx.StaticBox(self, label="Specify the shuffle") - shuffle_boxsizer = wx.StaticBoxSizer(shuffle_text, wx.VERTICAL) - self.shuffle = wx.SpinCtrl(self, value="1", min=0, max=100) - shuffle_boxsizer.Add(self.shuffle, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 1) - - trainingset = wx.StaticBox(self, label="Specify the trainingset index") - trainingset_boxsizer = wx.StaticBoxSizer(trainingset, wx.VERTICAL) - self.trainingset = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingset_boxsizer.Add(self.trainingset, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 1) - - hbox1.Add(videotype_text_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox1.Add(shuffle_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox1.Add(trainingset_boxsizer, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - boxsizer.Add(hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.sizer.Add( - boxsizer, - pos=(4, 0), - span=(1, 5), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=5, - ) - - self.cfg = auxiliaryfunctions.read_config(self.config) - if self.cfg.get("multianimalproject", False): - self.plot_idv = wx.RadioBox( - self, - label="Create video with animal ID colored?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.plot_idv.SetSelection(1) - hbox3.Add(self.plot_idv, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - - self.draw_skeleton = wx.RadioBox( - self, - label="Include the skeleton in the video?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.draw_skeleton.Bind(wx.EVT_RADIOBOX, self.choose_draw_skeleton_options) - self.draw_skeleton.SetSelection(1) - - self.filter = wx.RadioBox( - self, - label="Use filtered predictions?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.filter.SetSelection(1) - - self.video_slow = wx.RadioBox( - self, - label="Create a higher quality video? (slow)", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.video_slow.Bind(wx.EVT_RADIOBOX, self.choose_video_slow_options) - self.video_slow.SetSelection(1) - - self.trail_points_text = wx.StaticBox( - self, label="Specify the number of trail points" - ) - trail_pointsboxsizer = wx.StaticBoxSizer(self.trail_points_text, wx.VERTICAL) - self.trail_points = wx.SpinCtrl(self, value="0") - trail_pointsboxsizer.Add( - self.trail_points, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3 - ) - - self.bodypart_choice = wx.RadioBox( - self, - label="Plot all bodyparts?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.bodypart_choice.Bind(wx.EVT_RADIOBOX, self.chooseOption) - - config_file = auxiliaryfunctions.read_config(self.config) - if config_file.get("multianimalproject", False): - bodyparts = config_file["multianimalbodyparts"] - else: - bodyparts = config_file["bodyparts"] - self.bodyparts_to_compare = wx.CheckListBox( - self, choices=bodyparts, style=0, name="Select the bodyparts" - ) - self.bodyparts_to_compare.Bind(wx.EVT_CHECKLISTBOX, self.getbp) - self.bodyparts_to_compare.Hide() - - hbox2.Add(self.draw_skeleton, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - hbox2.Add(trail_pointsboxsizer, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - hbox2.Add(self.video_slow, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - boxsizer.Add(hbox2, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - hbox3.Add(self.filter, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - hbox4.Add(self.bodypart_choice, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - hbox4.Add(self.bodyparts_to_compare, 3, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - - if self.cfg.get("multianimalproject", False): - tracker_text = wx.StaticBox(self, label="Specify the Tracker Method!") - tracker_text_boxsizer = wx.StaticBoxSizer(tracker_text, wx.VERTICAL) - trackertypes = ["skeleton", "box", "ellipse"] - self.trackertypes = wx.ComboBox( - self, choices=trackertypes, style=wx.CB_READONLY - ) - self.trackertypes.SetValue("ellipse") - tracker_text_boxsizer.Add( - self.trackertypes, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - self.trajectory = wx.RadioBox( - self, - label="Want to plot the trajectories?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - - hbox4.Add(self.trajectory, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox4.Add(tracker_text_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - boxsizer.Add(hbox3, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(hbox4, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.build = wx.Button(self, label="Build skeleton") - self.sizer.Add(self.build, pos=(5, 3), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.build.Bind(wx.EVT_BUTTON, self.build_skeleton) - self.build.Enable(True) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(5, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="RUN") - self.sizer.Add(self.ok, pos=(5, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.create_videos) - - self.reset = wx.Button(self, label="Reset") - self.sizer.Add( - self.reset, pos=(5, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_create_videos) - - self.sizer.AddGrowableCol(3) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def build_skeleton(self, event): - skeleton.SkeletonBuilder(self.config) - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def select_videos(self, event): - """ - Selects the videos from the directory - """ - cwd = os.getcwd() - dlg = wx.FileDialog(self, "Select videos", cwd, "", "*.*", wx.FD_MULTIPLE) - if dlg.ShowModal() == wx.ID_OK: - self.vids = dlg.GetPaths() - self.filelist = self.filelist + self.vids - self.sel_vids.SetLabel("Total %s Videos selected" % len(self.filelist)) - - def choose_draw_skeleton_options(self, event): - if self.draw_skeleton.GetStringSelection() == "Yes": - self.draw = True - else: - self.draw = False - - def plot_idv_options(self, event): - if self.plot_idv.GetStringSelection() == "Yes": - self.plot_idv = "individual" - else: - self.plot_idv = "bodypart" - - def choose_video_slow_options(self, event): - if self.video_slow.GetStringSelection() == "Yes": - self.slow = True - else: - self.slow = False - - def chooseOption(self, event): - if self.bodypart_choice.GetStringSelection() == "No": - self.bodyparts_to_compare.Show() - self.getbp(event) - self.SetSizer(self.sizer) - self.sizer.Fit(self) # this sets location. - if self.bodypart_choice.GetStringSelection() == "Yes": - self.bodyparts_to_compare.Hide() - self.SetSizer(self.sizer) - self.sizer.Fit(self) - self.bodyparts = "all" - - def getbp(self, event): - self.bodyparts = list(self.bodyparts_to_compare.GetCheckedStrings()) - - def create_videos(self, event): - - shuffle = self.shuffle.GetValue() - trainingsetindex = self.trainingset.GetValue() - # self.filelist = self.filelist + self.vids - - if self.filter.GetStringSelection() == "No": - filtered = False - else: - filtered = True - - if self.video_slow.GetStringSelection() == "Yes": - self.slow = True - else: - self.slow = False - - if len(self.bodyparts) == 0: - self.bodyparts = "all" - - config_file = auxiliaryfunctions.read_config(self.config) - if config_file.get("multianimalproject", False): - print( - "Creating a video with the " - + self.trackertypes.GetValue() - + " tracker method!" - ) - if self.plot_idv.GetStringSelection() == "Yes": - color_by = "individual" - else: - color_by = "bodypart" - - deeplabcut.create_labeled_video( - self.config, - self.filelist, - self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - save_frames=self.slow, - draw_skeleton=self.draw, - displayedbodyparts=self.bodyparts, - trailpoints=self.trail_points.GetValue(), - filtered=filtered, - color_by=color_by, - track_method=self.trackertypes.GetValue(), - ) - - if self.trajectory.GetStringSelection() == "Yes": - deeplabcut.plot_trajectories( - self.config, - self.filelist, - displayedbodyparts=self.bodyparts, - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - filtered=filtered, - showfigures=False, - track_method=self.trackertypes.GetValue(), - ) - else: - deeplabcut.create_labeled_video( - self.config, - self.filelist, - self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - save_frames=self.slow, - draw_skeleton=self.draw, - displayedbodyparts=self.bodyparts, - trailpoints=self.trail_points.GetValue(), - filtered=filtered, - ) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.create_labeled_video" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - os.remove("help.txt") - - def reset_create_videos(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - self.videotype.SetStringSelection(".avi") - self.sel_vids.SetLabel("Select videos") - self.filelist = [] - self.shuffle.SetValue(1) - self.trainingset.SetValue(0) - if self.draw_skeleton.IsShown(): - self.draw_skeleton.SetSelection(1) - # self.SetSizer(self.sizer) - # self.sizer.Fit(self) - self.bodyparts_to_compare.Hide() diff --git a/deeplabcut/gui/dialogs/__init__.py b/deeplabcut/gui/dialogs/__init__.py new file mode 100644 index 0000000000..abf3848d70 --- /dev/null +++ b/deeplabcut/gui/dialogs/__init__.py @@ -0,0 +1,28 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from collections.abc import Sequence + +from .debug_dialog import ( + DebugTextDialog, + create_generate_debug_log_action, + make_issue_report_provider, + make_log_text_provider, + show_debug_report_dialog, +) + +__all__: Sequence[str] = ( + "DebugTextDialog", + "create_generate_debug_log_action", + "make_issue_report_provider", + "make_log_text_provider", + "show_debug_report_dialog", +) diff --git a/deeplabcut/gui/dialogs/config_errors.py b/deeplabcut/gui/dialogs/config_errors.py new file mode 100644 index 0000000000..2b47c129af --- /dev/null +++ b/deeplabcut/gui/dialogs/config_errors.py @@ -0,0 +1,132 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""User-facing formatting for project configuration errors.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from pydantic import ValidationError + +#: Exceptions that indicate a configuration could not be read or validated. +CONFIG_LOAD_ERRORS = ( + ValidationError, + FileNotFoundError, + PermissionError, + OSError, + TypeError, + ValueError, +) + +_CUSTOM_MESSAGES: dict[str, str] = { + "extra_forbidden": "This setting is not supported by the installed DeepLabCut version.", + "missing": "This required setting is missing.", +} + + +@dataclass(frozen=True) +class ConfigErrorReport: + """Description of a configuration error for presentation in the GUI.""" + + title: str + summary: str + details: str + technical_details: str + + +def _format_location(location: tuple[Any, ...]) -> str: + """Format a Pydantic error location as a readable field path.""" + if not location: + return "Configuration" + + parts: list[str] = [] + + for item in location: + if isinstance(item, int): + if parts: + parts[-1] = f"{parts[-1]}[{item}]" + else: + parts.append(f"[{item}]") + else: + parts.append(str(item)) + + return ".".join(parts) + + +def _format_input( + value: Any, + max_length: int = 120, +) -> str: + """Format an invalid value without flooding the dialog.""" + text = repr(value) + + if len(text) <= max_length: + return text + + return text[: max_length - 3] + "..." + + +def format_config_error( + config_path: str | Path, + error: Exception, +) -> ConfigErrorReport: + """Build a concise, user-facing report for a configuration error.""" + path = Path(config_path) + + if isinstance(error, ValidationError): + entries: list[str] = [] + + for detail in error.errors(include_url=False): + location = _format_location(tuple(detail.get("loc", ()))) + error_type = detail.get("type") + message = _CUSTOM_MESSAGES.get( + error_type, + detail.get("msg", "Invalid value"), + ) + + entry = f"• {location}: {message}" + + if "input" in detail: + entry += f"\n Received: {_format_input(detail['input'])}" + + entries.append(entry) + + count = error.error_count() + noun = "problem" if count == 1 else "problems" + + return ConfigErrorReport( + title="Invalid project configuration", + summary=(f"DeepLabCut found {count} {noun} in the project configuration."), + details=(f"Configuration file:\n{path}\n\n" + "\n\n".join(entries)), + technical_details=str(error), + ) + + if isinstance(error, FileNotFoundError): + return ConfigErrorReport( + title="Project configuration not found", + summary=("The selected project configuration does not exist."), + details=f"Configuration file:\n{path}", + technical_details=repr(error), + ) + + if isinstance(error, PermissionError): + return ConfigErrorReport( + title="Cannot read project configuration", + summary=("DeepLabCut does not have permission to read the selected configuration."), + details=f"Configuration file:\n{path}", + technical_details=repr(error), + ) + + return ConfigErrorReport( + title="Cannot load project configuration", + summary=("DeepLabCut could not read the selected project configuration."), + details=(f"Configuration file:\n{path}\n\n{error}"), + technical_details=repr(error), + ) diff --git a/deeplabcut/gui/dialogs/debug_dialog.py b/deeplabcut/gui/dialogs/debug_dialog.py new file mode 100644 index 0000000000..788284d631 --- /dev/null +++ b/deeplabcut/gui/dialogs/debug_dialog.py @@ -0,0 +1,321 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +from __future__ import annotations + +from collections.abc import Callable, Iterable + +from PySide6 import QtGui +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QFontDatabase, QKeySequence, QTextCursor +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QHBoxLayout, + QLabel, + QPlainTextEdit, + QPushButton, + QVBoxLayout, + QWidget, +) + +from deeplabcut.core.debug import ( + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + build_debug_report, + get_debug_recorder, + install_debug_recorder, +) + + +def make_log_text_provider( + *, + recorder: InMemoryDebugRecorder | None, + limit: int = 300, +) -> Callable[[], str]: + """Return a callable that renders recent captured logs.""" + + def _provider() -> str: + if recorder is None: + return "" + return recorder.render_text(limit=limit) + + return _provider + + +def make_issue_report_provider( + *, + recorder: InMemoryDebugRecorder | None, + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, +) -> Callable[[], str]: + """Return a callable that builds a full DLC debug report. + + ``libraries`` and ``executables`` are normalized to tuples so the returned + provider can be called repeatedly even if the caller passed a generator or + another one-shot iterable. + """ + libraries_snapshot = None if libraries is None else tuple(libraries) + executables_snapshot = None if executables is None else tuple(executables) + + def _provider() -> str: + return build_debug_report( + recorder=recorder, + libraries=libraries_snapshot, + executables=executables_snapshot, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + ) + + return _provider + + +class DebugTextDialog(QDialog): + """ + Minimal, application-agnostic debug text viewer. + + This widget only knows how to: + - fetch text from a callable + - display it read-only + - copy it to clipboard + - refresh it on demand + + It intentionally knows nothing about: + - recorder internals + - DLC main window internals + - environment/report formatting + """ + + def __init__( + self, + *, + title: str, + text_provider: Callable[[], str], + parent: QWidget | None = None, + initial_hint: str = "Read-only diagnostic output", + ) -> None: + super().__init__(parent=parent) + self.setWindowTitle(title) + self.setModal(False) + self.resize(950, 700) + + self._text_provider = text_provider + + self._build_ui(initial_hint=initial_hint) + + def update_content( + self, + *, + title: str | None = None, + text_provider: Callable[[], str] | None = None, + hint: str | None = None, + ) -> None: + """Update dialog metadata when reusing an existing instance.""" + if title is not None: + self.setWindowTitle(title) + if text_provider is not None: + self._text_provider = text_provider + if hint is not None: + self._hint_label.setText(hint) + + def _build_ui(self, *, initial_hint: str) -> None: + layout = QVBoxLayout(self) + + self._hint_label = QLabel(initial_hint, self) + self._hint_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + layout.addWidget(self._hint_label) + + self._text_edit = QPlainTextEdit(self) + self._text_edit.setReadOnly(True) + self._text_edit.setLineWrapMode(QPlainTextEdit.NoWrap) + + # Use a fixed-width system font for logs / reports + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + self._text_edit.setFont(font) + + layout.addWidget(self._text_edit, stretch=1) + + button_row = QHBoxLayout() + + self._status_label = QLabel("", self) + self._status_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + button_row.addWidget(self._status_label, stretch=1) + + self._refresh_btn = QPushButton("Refresh", self) + self._refresh_btn.clicked.connect(self.refresh_text) + button_row.addWidget(self._refresh_btn) + + self._copy_btn = QPushButton("Copy to clipboard", self) + self._copy_btn.clicked.connect(self.copy_to_clipboard) + button_row.addWidget(self._copy_btn) + + self._close_btn = QPushButton("Close", self) + self._close_btn.clicked.connect(self.close) + button_row.addWidget(self._close_btn) + + layout.addLayout(button_row) + + # Optional keyboard shortcut + copy_action = QAction(self) + copy_action.setShortcut(QKeySequence.StandardKey.Copy) + copy_action.triggered.connect(self.copy_to_clipboard) + self.addAction(copy_action) + + def refresh_text(self) -> None: + try: + QApplication.setOverrideCursor(Qt.WaitCursor) + text = self._text_provider() + except Exception as exc: + text = f"[debug-dialog] failed to build debug text\n\n{exc!r}" + finally: + QApplication.restoreOverrideCursor() + + self._text_edit.setPlainText(text or "") + self._text_edit.moveCursor(QTextCursor.MoveOperation.Start) + self._status_label.setText("") + + def copy_to_clipboard(self) -> None: + try: + text = self._text_edit.toPlainText() + QApplication.clipboard().setText(text) + self._status_label.setText("Copied to clipboard") + except Exception: + self._status_label.setText("Could not copy to clipboard") + + def showEvent(self, event: QtGui.QShowEvent) -> None: + """Refresh each time the dialog becomes visible.""" + super().showEvent(event) + self.refresh_text() + + +def _get_or_create_debug_dialog( + *, + parent: QWidget, + title: str, + text_provider: Callable[[], str], + text_hint: str, + attr_name: str = "_dlc_debug_dialog", +) -> DebugTextDialog: + """ + Reuse a single dialog instance attached to ``parent``. + + Storing the dialog on the main window avoids accidental garbage collection + and prevents opening a pile of duplicate windows. + """ + dlg = getattr(parent, attr_name, None) + if isinstance(dlg, DebugTextDialog): + dlg.update_content( + title=title, + text_provider=text_provider, + hint=text_hint, + ) + return dlg + + dlg = DebugTextDialog( + title=title, + text_provider=text_provider, + parent=parent, + initial_hint=text_hint, + ) + setattr(parent, attr_name, dlg) + return dlg + + +def show_debug_report_dialog( + *, + parent: QWidget, + recorder: InMemoryDebugRecorder | None = None, + logger_name: str = "deeplabcut", + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, + dialog_attr_name: str = "_dlc_debug_dialog", +) -> DebugTextDialog: + """ + Open (or reuse) the full diagnostic report dialog. + + If ``recorder`` is not provided, this function tries to reuse an existing + recorder for the given logger namespace and installs one if missing. + """ + if recorder is None: + recorder = get_debug_recorder(logger_name=logger_name) + if recorder is None: + recorder = install_debug_recorder(logger_name=logger_name) + + provider = make_issue_report_provider( + recorder=recorder, + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + ) + + dlg = _get_or_create_debug_dialog( + parent=parent, + title="DeepLabCut debug log", + text_provider=provider, + text_hint=("Diagnostic report for issue reporting. Use Refresh to update, then Copy to clipboard."), + attr_name=dialog_attr_name, + ) + # dlg.refresh_text() # redundant + dlg.show() + dlg.raise_() + dlg.activateWindow() + return dlg + + +def create_generate_debug_log_action( + *, + parent: QWidget, + recorder: InMemoryDebugRecorder | None = None, + logger_name: str = "deeplabcut", + libraries: Iterable[LibrarySpec | str] | None = None, + executables: Iterable[ExecutableSpec | str] | None = None, + include_module_paths: bool = False, + include_executable_paths: bool = True, + log_limit: int = 300, + text: str = "&Generate debug log...", + status_tip: str = "Generate a diagnostic report for troubleshooting", + dialog_attr_name: str = "_dlc_debug_dialog", +) -> QAction: + """ + Create a QAction that opens the DLC debug report dialog. + + Typical usage in ``MainWindow.create_actions``:: + + self.generateDebugLogAction = create_generate_debug_log_action(parent=self) + """ + action = QAction(text, parent) + action.setStatusTip(status_tip) + + def _open_dialog() -> None: + show_debug_report_dialog( + parent=parent, + recorder=recorder, + logger_name=logger_name, + libraries=libraries, + executables=executables, + include_module_paths=include_module_paths, + include_executable_paths=include_executable_paths, + log_limit=log_limit, + dialog_attr_name=dialog_attr_name, + ) + + action.triggered.connect(_open_dialog) + return action diff --git a/deeplabcut/gui/displays/__init__.py b/deeplabcut/gui/displays/__init__.py new file mode 100644 index 0000000000..117d127147 --- /dev/null +++ b/deeplabcut/gui/displays/__init__.py @@ -0,0 +1,10 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py new file mode 100644 index 0000000000..bc90fec53d --- /dev/null +++ b/deeplabcut/gui/displays/selected_shuffle_display.py @@ -0,0 +1,139 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Module to display information about the selected shuffle in the GUI.""" + +from __future__ import annotations + +from pathlib import Path + +import PySide6.QtCore as QtCore +from PySide6 import QtWidgets +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QSizePolicy + +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + + +class SelectedShuffleDisplay(QtWidgets.QWidget): + """A widget displaying information about the selected shuffle.""" + + pose_cfg_signal = QtCore.Signal(object) + + def __init__(self, root, row_margin: int = 25): + super().__init__() + self.root = root + + self._row_margin = row_margin + + self._current_index: int | None = None + self._engine: Engine | None = None + self._is_top_down: bool = False + self._net_type: str | None = None + self._pose_cfg: dict | None = None + + self._label = QtWidgets.QLabel("Shuffle info:") + self._label.setStyleSheet(f"margin: 0px 0px {self._row_margin}px 0px") + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self._label) + self.setLayout(layout) + + # initialize the display + self._update_display(self.root.shuffle_value) + + # update the display when the shuffle or selected engine changes, or when a new + # shuffle has been created + self.root.shuffle_change.connect(self._update_display) + self.root.engine_change.connect(self._update_display) + self.root.shuffle_created.connect(self._update_display) + + @property + def pose_cfg(self) -> dict | None: + return self._pose_cfg + + @pose_cfg.setter + def pose_cfg(self, value: dict | None) -> None: + self._pose_cfg = value + self.pose_cfg_signal.emit(self._pose_cfg) + + @QtCore.Slot(int) + def _update_display(self, new_index: int) -> None: + self._current_index = new_index + + try: + pose_cfg_path = Path(self.root.pose_cfg_path) + except ValueError: + 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 + self._set_text_error( + f"Failed to read shuffle {self._current_index} due to error `{err}`.\n" + "If the error is `ModuleNotFoundError: No module named 'tensorflow'`, " + f"this is because\nshuffle {self._current_index} uses the tensorflow " + " engine, but TensorFlow is not installed in your environment.\n" + "Ignore this error if you'll just train PyTorch models. To train " + "TensorFlow models, install it with \n" + " Windows/Linux: pip install 'deeplabcut[tf]'\n" + " Apple Silicon: pip install 'deeplabcut[apple_mchips]'" + ) + return + + if not pose_cfg_path.exists(): + self._set_text_error(f"The model configuration file:\n\n{pose_cfg_path}\n\nwas not created") + return + + self._read_pose_config(pose_cfg_path) + self._set_text() + + def _set_text(self) -> None: + engine_str = "None" + if self._engine is not None: + engine_str = self._engine.aliases[0] + + text = f"net type: {self._net_type} | engine: {engine_str}" + if self._engine == Engine.PYTORCH and self._is_top_down: + text += " | top-down" + + style = f"margin: 0px 0px {self._row_margin}px 0px;" + if self._engine != self.root.engine: + warning = "Change the selected Engine in the top-right to use this shuffle!" + text = warning + " | " + text + style += " color: orange;" + + self._label.setStyleSheet(style) + self._label.setText(text) + + def _set_text_error(self, error: str) -> None: + self._label.setWordWrap(True) + self._label.setTextFormat(Qt.PlainText) + self._label.setSizePolicy( + QSizePolicy.Policy.Minimum, + QSizePolicy.Policy.Expanding, + ) + self._label.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard + ) + + self._label.setText(error) + self._label.setStyleSheet(f"margin: 0 0 {self._row_margin}px 0; color: orange;") + self.pose_cfg = None + + def _read_pose_config(self, pose_cfg_path: Path) -> None: + pose_cfg = auxiliaryfunctions.read_plainconfig(pose_cfg_path) + + self._engine = Engine.PYTORCH if "pytorch" in pose_cfg_path.stem.lower() else Engine.TF + self._net_type = pose_cfg.get("net_type", "UNKNOWN") + + method = pose_cfg.get("method") + self._is_top_down = self._engine == Engine.PYTORCH and isinstance(method, str) and 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 new file mode 100644 index 0000000000..084d912643 --- /dev/null +++ b/deeplabcut/gui/displays/shuffle_metadata_viewer.py @@ -0,0 +1,65 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Widget to display existing shuffles.""" + +from __future__ import annotations + +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +import deeplabcut.generate_training_dataset.metadata as metadata + + +class ShuffleMetadataViewer(QtWidgets.QDialog): + """Viewer for shuffle metadata.""" + + def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): + super().__init__(parent) + self.root = root + self.parent = parent + self.file_content = _load_metadata(self.root.cfg) + + self.setWindowTitle("Existing Shuffles: Metadata") + self.setMinimumWidth(400) + self.setMinimumHeight(400) + + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(True) + + inner_layout = QtWidgets.QVBoxLayout() + inner_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + inner_layout.setSpacing(0) + inner_layout.setContentsMargins(0, 0, 0, 0) + + for line in self.file_content: + inner_layout.addWidget(QtWidgets.QLabel(line)) + + inner = QtWidgets.QFrame(scroll) + inner.setLayout(inner_layout) + scroll.setWidget(inner) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(scroll) + self.setLayout(layout) + + +def _load_metadata(cfg: dict) -> list[str]: + metadata_path = metadata.TrainingDatasetMetadata.path(cfg) + if not metadata_path.exists(): + trainset_meta = metadata.TrainingDatasetMetadata.create(cfg) + trainset_meta.save() + + with Path(metadata_path).open() as file: + raw_metadata = file.read() + + return raw_metadata.split("\n") diff --git a/deeplabcut/gui/dlc_params.py b/deeplabcut/gui/dlc_params.py new file mode 100644 index 0000000000..fce5d15a52 --- /dev/null +++ b/deeplabcut/gui/dlc_params.py @@ -0,0 +1,38 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +class DLCParams: + VIDEOTYPES = [ + "", + "avi", + "mp4", + "mkv", + "mov", + ] + + NNETS = [ + "dlcrnet_ms5", + "resnet_50", + "resnet_101", + "resnet_152", + "mobilenet_v2_1.0", + "mobilenet_v2_0.75", + "mobilenet_v2_0.5", + "mobilenet_v2_0.35", + "efficientnet-b0", + "efficientnet-b3", + "efficientnet-b6", + ] + + FRAME_EXTRACTION_ALGORITHMS = ["kmeans", "uniform"] + + OUTLIER_EXTRACTION_ALGORITHMS = ["jump", "fitting", "uncertain", "manual"] + + TRACKERS = ["ellipse", "box", "skeleton"] diff --git a/deeplabcut/gui/evaluate_network.py b/deeplabcut/gui/evaluate_network.py deleted file mode 100644 index ddc1f8dfc4..0000000000 --- a/deeplabcut/gui/evaluate_network.py +++ /dev/null @@ -1,289 +0,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 -import pydoc -import subprocess -import sys -import webbrowser - -import wx - -import deeplabcut -from deeplabcut.gui import LOGO_PATH -from deeplabcut.utils import auxiliaryfunctions - - -class Evaluate_network(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.config = cfg - self.bodyparts = [] - # design the panel - self.sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 6. Evaluate Network") - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - self.sizer.Add( - icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5 - ) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - sb = wx.StaticBox(self, label="Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox2 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox3 = wx.BoxSizer(wx.HORIZONTAL) - - config_file = auxiliaryfunctions.read_config(self.config) - - shuffles_text = wx.StaticBox(self, label="Specify the shuffle") - shuffles_text_boxsizer = wx.StaticBoxSizer(shuffles_text, wx.VERTICAL) - self.shuffles = wx.SpinCtrl(self, value="1", min=0, max=100) - shuffles_text_boxsizer.Add(self.shuffles, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - trainingset = wx.StaticBox(self, label="Specify the trainingset index") - trainingset_boxsizer = wx.StaticBoxSizer(trainingset, wx.VERTICAL) - self.trainingset = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingset_boxsizer.Add( - self.trainingset, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - self.plot_choice = wx.RadioBox( - self, - label="Want to plot predictions (as in standard DLC projects)?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.plot_choice.SetSelection(0) - - self.plot_scoremaps = wx.RadioBox( - self, - label="Want to plot maps (ALL images): scoremaps, PAFs, locrefs?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.plot_scoremaps.SetSelection(1) - - self.bodypart_choice = wx.RadioBox( - self, - label="Compare all bodyparts?", - choices=["Yes", "No"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.bodypart_choice.Bind(wx.EVT_RADIOBOX, self.chooseOption) - - if config_file.get("multianimalproject", False): - bodyparts = config_file["multianimalbodyparts"] - else: - bodyparts = config_file["bodyparts"] - self.bodyparts_to_compare = wx.CheckListBox( - self, choices=bodyparts, style=0, name="Select the bodyparts" - ) - self.bodyparts_to_compare.Bind(wx.EVT_CHECKLISTBOX, self.getbp) - self.bodyparts_to_compare.Hide() - - self.hbox1.Add(shuffles_text_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox1.Add(trainingset_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox1.Add(self.plot_scoremaps, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.hbox2.Add(self.plot_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox2.Add(self.bodypart_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.hbox2.Add(self.bodyparts_to_compare, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - boxsizer.Add(self.hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(self.hbox2, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.sizer.Add( - boxsizer, - pos=(3, 0), - span=(1, 5), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(4, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="RUN: Evaluate Network") - self.sizer.Add(self.ok, pos=(4, 3)) - self.ok.Bind(wx.EVT_BUTTON, self.evaluate_network) - - self.ok = wx.Button(self, label="Optional: Plot 3 test maps") - self.sizer.Add(self.ok, pos=(5, 3)) - self.ok.Bind(wx.EVT_BUTTON, self.plot_maps) - - self.cancel = wx.Button(self, label="Reset") - self.sizer.Add( - self.cancel, pos=(4, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.cancel.Bind(wx.EVT_BUTTON, self.cancel_evaluate_network) - - if config_file.get("multianimalproject", False): - self.inf_cfg_text = wx.Button(self, label="Edit the inference_config.yaml") - self.inf_cfg_text.Bind(wx.EVT_BUTTON, self.edit_inf_config) - self.sizer.Add( - self.inf_cfg_text, - pos=(4, 2), - span=(1, 1), - flag=wx.BOTTOM | wx.RIGHT, - border=10, - ) - - self.sizer.AddGrowableCol(2) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.evaluate_network" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def chooseOption(self, event): - if self.bodypart_choice.GetStringSelection() == "No": - self.bodyparts_to_compare.Show() - self.getbp(event) - self.SetSizer(self.sizer) - self.sizer.Fit(self) - if self.bodypart_choice.GetStringSelection() == "Yes": - self.bodyparts_to_compare.Hide() - self.SetSizer(self.sizer) - self.sizer.Fit(self) - self.bodyparts = "all" - - def getbp(self, event): - self.bodyparts = list(self.bodyparts_to_compare.GetCheckedStrings()) - - def select_config(self, event): - self.config = self.sel_config.GetPath() - - def plot_maps(self, event): - shuffle = self.shuffles.GetValue() - # if self.plot_scoremaps.GetStringSelection() == "Yes": - deeplabcut.extract_save_all_maps( - self.config, shuffle=shuffle, Indices=[0, 1, 5] - ) - - def edit_inf_config(self, event): - # Read the infer config file - cfg = auxiliaryfunctions.read_config(self.config) - trainingsetindex = self.trainingset.GetValue() - trainFraction = cfg["TrainingFraction"][trainingsetindex] - self.inf_cfg_path = os.path.join( - cfg["project_path"], - auxiliaryfunctions.GetModelFolder( - trainFraction, self.shuffles.GetValue(), cfg - ), - "test", - "inference_cfg.yaml", - ) - # let the user open the file with default text editor. Also make it mac compatible - if sys.platform == "darwin": - self.file_open_bool = subprocess.call(["open", self.inf_cfg_path]) - self.file_open_bool = True - else: - self.file_open_bool = webbrowser.open(self.inf_cfg_path) - if self.file_open_bool: - self.inf_cfg = auxiliaryfunctions.read_config(self.inf_cfg_path) - else: - raise FileNotFoundError("File not found!") - - def evaluate_network(self, event): - - # shuffle = self.shuffle.GetValue() - trainingsetindex = self.trainingset.GetValue() - - Shuffles = [self.shuffles.GetValue()] - if self.plot_choice.GetStringSelection() == "Yes": - plotting = True - else: - plotting = False - - if self.plot_scoremaps.GetStringSelection() == "Yes": - for shuffle in Shuffles: - deeplabcut.extract_save_all_maps(self.config, shuffle=shuffle) - - if len(self.bodyparts) == 0: - self.bodyparts = "all" - deeplabcut.evaluate_network( - self.config, - Shuffles=Shuffles, - trainingsetindex=trainingsetindex, - plotting=plotting, - show_errors=True, - comparisonbodyparts=self.bodyparts, - ) - - def cancel_evaluate_network(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - self.plot_choice.SetSelection(1) - self.bodypart_choice.SetSelection(0) - self.shuffles.SetValue(1) - if self.bodyparts_to_compare.IsShown(): - self.bodyparts_to_compare.Hide() diff --git a/deeplabcut/gui/extract_frames.py b/deeplabcut/gui/extract_frames.py deleted file mode 100644 index cd67ba9fe7..0000000000 --- a/deeplabcut/gui/extract_frames.py +++ /dev/null @@ -1,262 +0,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 -import pydoc -import sys - -import wx - -from deeplabcut.generate_training_dataset import extract_frames -from deeplabcut.gui import LOGO_PATH - - -class Extract_frames(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.method = "automatic" - self.config = cfg - # design the panel - sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 2. Extract Frames") - sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - sizer.Add(icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5) - - line1 = wx.StaticLine(self) - sizer.Add(line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - sb = wx.StaticBox(self, label="Optional Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - hbox1 = wx.BoxSizer(wx.HORIZONTAL) - hbox2 = wx.BoxSizer(wx.HORIZONTAL) - hbox3 = wx.BoxSizer(wx.HORIZONTAL) - - self.method_choice = wx.RadioBox( - self, - label="Choose the extraction method", - choices=["automatic", "manual"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.method_choice.Bind(wx.EVT_RADIOBOX, self.select_extract_method) - hbox1.Add(self.method_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.crop_choice = wx.RadioBox( - self, - label="Want to crop the frames?", - choices=["False", "True (read from config file)", "GUI"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - hbox1.Add(self.crop_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.feedback_choice = wx.RadioBox( - self, - label="Need user feedback?", - choices=["No", "Yes"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - hbox1.Add(self.feedback_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - self.opencv_choice = wx.RadioBox( - self, - label="Want to use openCV?", - choices=["No", "Yes"], - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - ) - self.opencv_choice.SetSelection(1) - hbox1.Add(self.opencv_choice, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - algo_text = wx.StaticBox(self, label="Select the algorithm") - algoboxsizer = wx.StaticBoxSizer(algo_text, wx.VERTICAL) - self.algo_choice = wx.ComboBox(self, style=wx.CB_READONLY) - options = ["kmeans", "uniform"] - self.algo_choice.Set(options) - self.algo_choice.SetValue("kmeans") - algoboxsizer.Add(self.algo_choice, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - cluster_step_text = wx.StaticBox(self, label="Specify the cluster step") - cluster_stepboxsizer = wx.StaticBoxSizer(cluster_step_text, wx.VERTICAL) - self.cluster_step = wx.SpinCtrl(self, value="1") - cluster_stepboxsizer.Add( - self.cluster_step, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - slider_width_text = wx.StaticBox(self, label="Specify the GUI slider width") - slider_widthboxsizer = wx.StaticBoxSizer(slider_width_text, wx.VERTICAL) - self.slider_width = wx.SpinCtrl(self, value="25") - slider_widthboxsizer.Add( - self.slider_width, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - hbox3.Add(algoboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox3.Add(cluster_stepboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox3.Add(slider_widthboxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - boxsizer.Add(hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(hbox2, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(hbox3, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - sizer.Add( - boxsizer, - pos=(3, 0), - span=(1, 5), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.help_button = wx.Button(self, label="Help") - sizer.Add(self.help_button, pos=(4, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="Ok") - sizer.Add(self.ok, pos=(4, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.extract_frames) - - self.reset = wx.Button(self, label="Reset") - sizer.Add( - self.reset, pos=(4, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_extract_frames) - - sizer.AddGrowableCol(2) - - self.SetSizer(sizer) - sizer.Fit(self) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.extract_frames" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def select_extract_method(self, event): - self.method = self.method_choice.GetStringSelection() - if self.method == "manual": - self.crop_choice.Enable(False) - self.feedback_choice.Enable(False) - self.opencv_choice.Enable(False) - self.algo_choice.Enable(False) - self.cluster_step.Enable(False) - self.slider_width.Enable(False) - else: - self.crop_choice.Enable(True) - self.feedback_choice.Enable(True) - self.opencv_choice.Enable(True) - self.algo_choice.Enable(True) - self.cluster_step.Enable(True) - self.slider_width.Enable(True) - - def extract_frames(self, event): - mode = self.method - algo = self.algo_choice.GetValue() - if self.crop_choice.GetStringSelection() == "True (read from config file)": - crop = True - elif self.crop_choice.GetStringSelection() == "GUI": - crop = "GUI" - else: - crop = False - - if self.feedback_choice.GetStringSelection() == "Yes": - userfeedback = True - else: - userfeedback = False - - if self.opencv_choice.GetStringSelection() == "Yes": - opencv = True - else: - opencv = False - - slider_width = self.slider_width.GetValue() - extract_frames( - self.config, - mode, - algo, - crop=crop, - userfeedback=userfeedback, - cluster_step=self.cluster_step.GetValue(), - cluster_resizewidth=30, - cluster_color=False, - opencv=opencv, - slider_width=slider_width, - ) - - def reset_extract_frames(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - self.method_choice.SetStringSelection("automatic") - self.crop_choice.Enable(True) - self.feedback_choice.Enable(True) - self.opencv_choice.Enable(True) - self.algo_choice.Enable(True) - self.cluster_step.Enable(True) - self.slider_width.Enable(True) - self.crop_choice.SetStringSelection("False") - self.feedback_choice.SetStringSelection("No") - self.opencv_choice.SetStringSelection("Yes") - self.algo_choice.SetValue("kmeans") - self.cluster_step.SetValue(1) - self.slider_width.SetValue(25) diff --git a/deeplabcut/gui/extract_outlier_frames.py b/deeplabcut/gui/extract_outlier_frames.py deleted file mode 100644 index ef1248d054..0000000000 --- a/deeplabcut/gui/extract_outlier_frames.py +++ /dev/null @@ -1,227 +0,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 -import pydoc -import sys - -import wx - -import deeplabcut -from deeplabcut import utils - -from deeplabcut.gui import LOGO_PATH - - -class Extract_outlier_frames(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.config = cfg - self.cfg = utils.read_config(cfg) - self.filelist = [] - # design the panel - self.sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 8. Extract outlier frames") - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - self.sizer.Add( - icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5 - ) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - self.vids = wx.StaticText(self, label="Choose the videos") - self.sizer.Add(self.vids, pos=(3, 0), flag=wx.TOP | wx.LEFT, border=10) - - self.sel_vids = wx.Button(self, label="Select videos to analyze") - self.sizer.Add(self.sel_vids, pos=(3, 1), flag=wx.TOP | wx.EXPAND, border=5) - self.sel_vids.Bind(wx.EVT_BUTTON, self.select_videos) - - sb = wx.StaticBox(self, label="Optional Attributes") - boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - - hbox1 = wx.BoxSizer(wx.HORIZONTAL) - hbox2 = wx.BoxSizer(wx.HORIZONTAL) - - videotype_text = wx.StaticBox(self, label="Specify the videotype") - videotype_text_boxsizer = wx.StaticBoxSizer(videotype_text, wx.VERTICAL) - videotypes = [".avi", ".mp4", ".mov"] - self.videotype = wx.ComboBox(self, choices=videotypes, style=wx.CB_READONLY) - self.videotype.SetValue(".avi") - videotype_text_boxsizer.Add( - self.videotype, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - shuffles_text = wx.StaticBox(self, label="Specify the shuffle") - shuffles_text_boxsizer = wx.StaticBoxSizer(shuffles_text, wx.VERTICAL) - self.shuffles = wx.SpinCtrl(self, value="1", min=0, max=100) - shuffles_text_boxsizer.Add(self.shuffles, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - trainingindex = wx.StaticBox(self, label="Specify the trainingset index") - trainingindex_boxsizer = wx.StaticBoxSizer(trainingindex, wx.VERTICAL) - self.trainingindex = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingindex_boxsizer.Add( - self.trainingindex, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - outlier_algo_text = wx.StaticBox(self, label="Specify the algorithm") - outlier_algo_text_boxsizer = wx.StaticBoxSizer(outlier_algo_text, wx.VERTICAL) - algotypes = ["jump", "fitting", "uncertain", "manual"] - self.algotype = wx.ComboBox(self, choices=algotypes, style=wx.CB_READONLY) - self.algotype.SetValue("jump") - outlier_algo_text_boxsizer.Add( - self.algotype, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - hbox1.Add(videotype_text_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox1.Add(shuffles_text_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox1.Add(trainingindex_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox2.Add(outlier_algo_text_boxsizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - if self.cfg.get("multianimalproject", False): - tracker_text = wx.StaticBox(self, label="Specify the Tracker Method!") - tracker_text_boxsizer = wx.StaticBoxSizer(tracker_text, wx.VERTICAL) - trackertypes = ["skeleton", "box", "ellipse"] - self.trackertypes = wx.ComboBox( - self, choices=trackertypes, style=wx.CB_READONLY - ) - self.trackertypes.SetValue("ellipse") - tracker_text_boxsizer.Add( - self.trackertypes, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - hbox2.Add(tracker_text_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - boxsizer.Add(hbox1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - boxsizer.Add(hbox2, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - self.sizer.Add( - boxsizer, - pos=(4, 0), - span=(1, 5), - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10, - ) - - self.help_button = wx.Button(self, label="Help") - self.sizer.Add(self.help_button, pos=(6, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="Ok") - self.sizer.Add(self.ok, pos=(6, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.extract_outlier_frames) - - self.reset = wx.Button(self, label="Reset") - self.sizer.Add( - self.reset, pos=(6, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_extract_outlier_frames) - - self.sizer.AddGrowableCol(2) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.extract_outlier_frames" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def select_videos(self, event): - """ - Selects the videos from the directory - """ - cwd = os.getcwd() - dlg = wx.FileDialog( - self, "Select videos to analyze", cwd, "", "*.*", wx.FD_MULTIPLE - ) - if dlg.ShowModal() == wx.ID_OK: - self.vids = dlg.GetPaths() - self.filelist = self.filelist + self.vids - self.sel_vids.SetLabel("Total %s Videos selected" % len(self.filelist)) - - def extract_outlier_frames(self, event): - tracker = "" - if self.cfg.get("multianimalproject", False): - tracker = self.trackertypes.GetValue() - - deeplabcut.extract_outlier_frames( - config=self.config, - videos=self.filelist, - videotype=self.videotype.GetValue(), - shuffle=self.shuffles.GetValue(), - trainingsetindex=self.trainingindex.GetValue(), - outlieralgorithm=self.algotype.GetValue(), - track_method=tracker, - ) - - def reset_extract_outlier_frames(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - self.videotype.SetValue(".avi") - self.algotype.SetValue("jump") - self.sel_vids.SetLabel("Select videos to analyze") - self.SetSizer(self.sizer) - self.sizer.Fit(self) diff --git a/deeplabcut/gui/frame_extraction_toolbox.py b/deeplabcut/gui/frame_extraction_toolbox.py deleted file mode 100644 index 9fc739d9d6..0000000000 --- a/deeplabcut/gui/frame_extraction_toolbox.py +++ /dev/null @@ -1,495 +0,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 argparse -import os -from pathlib import Path - -import cv2 -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import wx -from matplotlib.figure import Figure -from matplotlib.widgets import RectangleSelector -from mpl_toolkits.axes_grid1 import make_axes_locatable -from skimage import io -from skimage.util import img_as_ubyte - -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import auxiliaryfunctions - - -# ########################################################################### -# Class for GUI MainFrame -# ########################################################################### -class ImagePanel(BasePanel): - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - im = io.imread(img) - norm = mcolors.Normalize(vmin=0, vmax=np.max(im)) - ticks = np.linspace(0, np.max(im), len(bodyparts))[::-1] - return norm, ticks - - -class MainFrame(BaseFrame): - def __init__(self, parent, config, slider_width=25): - super(MainFrame, self).__init__( - "DeepLabCut2.0 - Manual Frame Extraction", parent - ) - - ################################################################################################################################################### - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - topSplitter = wx.SplitterWindow(self) - - self.image_panel = ImagePanel(topSplitter, config, self.gui_size) - self.widget_panel = WidgetPanel(topSplitter) - - topSplitter.SplitHorizontally( - self.image_panel, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - - self.load = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Load Video") - widgetsizer.Add(self.load, 1, wx.ALL, 15) - self.load.Bind(wx.EVT_BUTTON, self.browseDir) - - self.help = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Help") - widgetsizer.Add(self.help, 1, wx.ALL, 15) - self.help.Bind(wx.EVT_BUTTON, self.helpButton) - - self.grab = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Grab Frames") - widgetsizer.Add(self.grab, 1, wx.ALL, 15) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.grab.Enable(False) - - widgetsizer.AddStretchSpacer(5) - size_x = round(self.gui_size[0] * (slider_width / 100), 0) - self.slider = wx.Slider( - self.widget_panel, - id=wx.ID_ANY, - value=0, - minValue=0, - maxValue=1, - size=(size_x, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - widgetsizer.Add(self.slider, 1, wx.ALL, 5) - self.slider.Hide() - - widgetsizer.AddStretchSpacer(5) - self.start_frames_sizer = wx.BoxSizer(wx.VERTICAL) - self.end_frames_sizer = wx.BoxSizer(wx.VERTICAL) - - self.start_frames_sizer.AddSpacer(15) - self.startFrame = wx.SpinCtrl( - self.widget_panel, value="0", size=(100, -1), min=0, max=120 - ) - self.startFrame.Bind(wx.EVT_SPINCTRL, self.updateSlider) - self.startFrame.Enable(False) - self.start_frames_sizer.Add(self.startFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - start_text = wx.StaticText(self.widget_panel, label="Start Frame Index") - self.start_frames_sizer.Add(start_text, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.checkBox = wx.CheckBox( - self.widget_panel, id=wx.ID_ANY, label="Range of frames" - ) - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activate_frame_range) - self.start_frames_sizer.Add(self.checkBox, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - # - self.end_frames_sizer.AddSpacer(15) - self.endFrame = wx.SpinCtrl( - self.widget_panel, value="1", size=(160, -1), min=1, max=120 - ) - self.endFrame.Enable(False) - self.end_frames_sizer.Add(self.endFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - end_text = wx.StaticText(self.widget_panel, label="Number of Frames") - self.end_frames_sizer.Add(end_text, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.updateFrame = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Update") - self.end_frames_sizer.Add(self.updateFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.updateFrame.Bind(wx.EVT_BUTTON, self.updateSlider) - self.updateFrame.Enable(False) - - widgetsizer.Add(self.start_frames_sizer, 1, wx.ALL, 0) - widgetsizer.AddStretchSpacer(5) - widgetsizer.Add(self.end_frames_sizer, 1, wx.ALL, 0) - widgetsizer.AddStretchSpacer(15) - - self.quit = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Quit") - widgetsizer.Add(self.quit, 1, wx.ALL, 15) - self.quit.Bind(wx.EVT_BUTTON, self.quitButton) - self.quit.Enable(True) - - # Hiding these widgets and show them once the video is loaded - self.start_frames_sizer.ShowItems(show=False) - self.end_frames_sizer.ShowItems(show=False) - - self.widget_panel.SetSizer(widgetsizer) - self.widget_panel.SetSizerAndFit(widgetsizer) - self.widget_panel.Layout() - - # Variables initialization - self.numberFrames = 0 - self.currFrame = 0 - self.figure = Figure() - self.axes = self.figure.add_subplot(111) - self.drs = [] - self.cfg = auxiliaryfunctions.read_config(config) - self.Task = self.cfg["Task"] - self.start = self.cfg["start"] - self.stop = self.cfg["stop"] - self.date = self.cfg["date"] - self.trainFraction = self.cfg["TrainingFraction"] - self.trainFraction = self.trainFraction[0] - self.videos = list( - self.cfg.get("video_sets_original") or self.cfg["video_sets"] - ) - self.bodyparts = self.cfg["bodyparts"] - self.colormap = plt.get_cmap(self.cfg["colormap"]) - self.colormap = self.colormap.reversed() - self.markerSize = self.cfg["dotsize"] - self.alpha = self.cfg["alphavalue"] - self.video_names = [Path(i).stem for i in self.videos] - self.config_path = Path(config) - self.extract_range_frame = False - self.extract_from_analyse_video = False - - def quitButton(self, event): - """ - Quits the GUI - """ - self.statusbar.SetStatusText("") - dlg = wx.MessageDialog( - None, "Are you sure?", "Quit!", wx.YES_NO | wx.ICON_WARNING - ) - result = dlg.ShowModal() - if result == wx.ID_YES: - print("Quitting for now!") - self.Destroy() - - def updateSlider(self, event): - self.slider.SetValue(self.startFrame.GetValue()) - self.currFrame = self.slider.GetValue() - if self.extract_from_analyse_video: - self.figure.delaxes(self.figure.axes[1]) - self.plot_labels() - self.update() - - def activate_frame_range(self, event): - """ - Activates the frame range boxes - """ - self.checkSlider = event.GetEventObject() - if self.checkSlider.GetValue(): - self.extract_range_frame = True - self.startFrame.Enable(True) - self.startFrame.SetValue(self.slider.GetValue()) - self.endFrame.Enable(True) - self.updateFrame.Enable(True) - self.grab.Enable(False) - else: - self.extract_range_frame = False - self.startFrame.Enable(False) - self.endFrame.Enable(False) - self.updateFrame.Enable(False) - self.grab.Enable(True) - - def line_select_callback(self, eclick, erelease): - "eclick and erelease are the press and release events" - self.new_x1, self.new_y1 = eclick.xdata, eclick.ydata - self.new_x2, self.new_y2 = erelease.xdata, erelease.ydata - - def CheckCropping(self): - """ Display frame at time "time" for video to check if cropping is fine. - Select ROI of interest by adjusting values in myconfig.py - - USAGE for cropping: - clip.crop(x1=None, y1=None, x2=None, y2=None, width=None, height=None, x_center=None, y_center=None) - - Returns a new clip in which just a rectangular subregion of the - original clip is conserved. x1,y1 indicates the top left corner and - x2,y2 is the lower right corner of the cropped region. - - All coordinates are in pixels. Float numbers are accepted. - """ - - videosource = self.video_source - try: - self.x1 = int(self.cfg["video_sets"][videosource]["crop"].split(",")[0]) - self.x2 = int(self.cfg["video_sets"][videosource]["crop"].split(",")[1]) - self.y1 = int(self.cfg["video_sets"][videosource]["crop"].split(",")[2]) - self.y2 = int(self.cfg["video_sets"][videosource]["crop"].split(",")[3]) - except KeyError: - self.x1, self.x2, self.y1, self.y2 = map( - int, self.cfg["video_sets_original"][videosource]["crop"].split(",") - ) - - if self.cropping: - # Select ROI of interest by drawing a rectangle - self.cid = RectangleSelector( - self.axes, - self.line_select_callback, - drawtype="box", - useblit=False, - button=[1], - minspanx=5, - minspany=5, - spancoords="pixels", - interactive=True, - ) - self.canvas.mpl_connect("key_press_event", self.cid) - - def OnSliderScroll(self, event): - """ - Slider to scroll through the video - """ - self.axes.clear() - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.currFrame = self.slider.GetValue() - self.startFrame.SetValue(self.currFrame) - self.update() - - def is_crop_ok(self, event): - """ - Checks if the cropping is ok - """ - - self.grab.SetLabel("Grab Frames") - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.slider.Show() - self.start_frames_sizer.ShowItems(show=True) - self.end_frames_sizer.ShowItems(show=True) - self.widget_panel.Layout() - self.slider.SetMax(self.numberFrames) - self.startFrame.SetMax(self.numberFrames - 1) - self.endFrame.SetMax(self.numberFrames) - self.x1 = int(self.new_x1) - self.x2 = int(self.new_x2) - self.y1 = int(self.new_y1) - self.y2 = int(self.new_y2) - self.canvas.mpl_disconnect(self.cid) - self.axes.clear() - self.currFrame = self.slider.GetValue() - self.update() - # Update the config.yaml file - self.cfg["video_sets"][self.video_source] = { - "crop": ", ".join(map(str, [self.x1, self.x2, self.y1, self.y2])) - } - auxiliaryfunctions.write_config(self.config_path, self.cfg) - - def browseDir(self, event): - """ - Show the File Dialog and ask the user to select the video file - """ - - self.statusbar.SetStatusText("Looking for a video to start extraction..") - dlg = wx.FileDialog(self, "SELECT A VIDEO", os.getcwd(), "", "*.*", wx.FD_OPEN) - if dlg.ShowModal() == wx.ID_OK: - self.video_source_original = dlg.GetPath() - self.video_source = str(Path(self.video_source_original).resolve()) - - self.load.Enable(False) - else: - pass - dlg.Destroy() - self.Close(True) - dlg.Destroy() - selectedvideo = Path(self.video_source) - - self.statusbar.SetStatusText( - "Working on video: {}".format(os.path.split(str(selectedvideo))[-1]) - ) - - if str(selectedvideo.stem) in self.video_names: - self.grab.Enable(True) - self.vid = cv2.VideoCapture(self.video_source) - self.videoPath = os.path.dirname(self.video_source) - self.filename = Path(self.video_source).name - self.numberFrames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT)) - # Checks if the video is corrupt. - if not self.vid.isOpened(): - msg = wx.MessageBox( - "Invalid Video file!Do you want to retry?", - "Error!", - wx.YES_NO | wx.ICON_WARNING, - ) - if msg == 2: - self.load.Enable(True) - MainFrame.browseDir(self, event) - else: - self.Destroy() - self.slider.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - self.update() - - cropMsg = wx.MessageBox( - "Do you want to crop the frames?", - "Want to crop?", - wx.YES_NO | wx.ICON_INFORMATION, - ) - if cropMsg == 2: - self.cropping = True - self.grab.SetLabel("Set cropping parameters") - self.grab.Bind(wx.EVT_BUTTON, self.is_crop_ok) - self.widget_panel.Layout() - self.basefolder = "data-" + self.Task + "/" - MainFrame.CheckCropping(self) - else: - self.cropping = False - self.slider.Show() - self.start_frames_sizer.ShowItems(show=True) - self.end_frames_sizer.ShowItems(show=True) - self.widget_panel.Layout() - self.slider.SetMax(self.numberFrames - 1) - self.startFrame.SetMax(self.numberFrames - 1) - self.endFrame.SetMax(self.numberFrames - 1) - - else: - wx.MessageBox( - "Video file is not in config file. Use add function to add this video in the config file and retry!", - "Error!", - wx.OK | wx.ICON_WARNING, - ) - self.Close(True) - - def update(self): - """ - Updates the image with the current slider index - """ - self.grab.Enable(True) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.figure, self.axes, self.canvas = self.image_panel.getfigure() - self.vid.set(1, self.currFrame) - ret, frame = self.vid.read() - if ret: - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - self.ax = self.axes.imshow(frame) - self.axes.set_title( - str( - str(self.currFrame) - + "/" - + str(self.numberFrames - 1) - + " " - + self.filename - ) - ) - self.figure.canvas.draw() - - def chooseFrame(self): - ret, frame = self.vid.read() - fname = Path(self.filename) - output_path = self.config_path.parents[0] / "labeled-data" / fname.stem - - if output_path.exists(): - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - frame = img_as_ubyte(frame) - img_name = ( - str(output_path) - + "/img" - + str(self.currFrame).zfill(int(np.ceil(np.log10(self.numberFrames)))) - + ".png" - ) - if self.cropping: - crop_img = frame[self.y1 : self.y2, self.x1 : self.x2] - cv2.imwrite(img_name, cv2.cvtColor(crop_img, cv2.COLOR_RGB2BGR)) - else: - cv2.imwrite(img_name, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) - else: - print( - "%s path not found. Please make sure that the video was added to the config file using the function 'deeplabcut.add_new_videos'." - % output_path - ) - - def grabFrame(self, event): - """ - Extracts the frame and saves in the current directory - """ - num_frames_extract = self.endFrame.GetValue() - for i in range(self.currFrame, self.currFrame + num_frames_extract): - self.currFrame = i - self.vid.set(1, self.currFrame) - self.chooseFrame() - self.vid.set(1, self.currFrame) - self.chooseFrame() - - def plot_labels(self): - """ - Plots the labels of the analyzed video - """ - self.vid.set(1, self.currFrame) - ret, frame = self.vid.read() - if ret: - frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - self.norm = mcolors.Normalize(vmin=np.min(frame), vmax=np.max(frame)) - self.colorIndex = np.linspace( - np.min(frame), np.max(frame), len(self.bodyparts) - ) - divider = make_axes_locatable(self.axes) - cax = divider.append_axes("right", size="5%", pad=0.05) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.bodyparts) - for bpindex, bp in enumerate(self.bodyparts): - color = self.colormap(self.norm(self.colorIndex[bpindex])) - self.points = [ - self.Dataframe[self.scorer][bp]["x"].values[self.currFrame], - self.Dataframe[self.scorer][bp]["y"].values[self.currFrame], - 1.0, - ] - circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - alpha=self.alpha, - ) - ] - self.axes.add_patch(circle[0]) - self.figure.canvas.draw() - - def helpButton(self, event): - """ - Opens Instructions - """ - wx.MessageBox( - "1. Use the Load Video button to load a video. Use the slider to select a frame in the entire video. The number mentioned on the top of the slider represents the frame index. \n\n2. Click Grab Frames button to save the specific frame.\n\n3. In events where you need to extract a range of frames, then use the checkbox Range of frames to select the start frame index and number of frames to extract. Click the update button to see the start frame index. Click Grab Frames to select the range of frames. \n\n Click OK to continue", - "Instructions to use!", - wx.OK | wx.ICON_INFORMATION, - ) - - -class MatplotPanel(wx.Panel): - def __init__(self, parent, config): - wx.Panel.__init__(self, parent, -1, size=(100, 100)) - - self.figure = Figure() - self.axes = self.figure.add_subplot(111) - - -def show(config, slider_width=25): - app = wx.App() - frame = MainFrame(None, config, slider_width).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/gui_assets.py b/deeplabcut/gui/gui_assets.py new file mode 100644 index 0000000000..85dcd603d3 --- /dev/null +++ b/deeplabcut/gui/gui_assets.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +from importlib.resources import files +from pathlib import Path + +from PySide6.QtGui import QIcon, QPixmap + +ASSETS_DIR = files("deeplabcut.gui").joinpath("assets") + + +def resource_bytes(*parts: str) -> bytes: + """Read a resource bundled inside deeplabcut.gui.""" + return ASSETS_DIR.joinpath(*parts).read_bytes() + + +def resource_text(*parts: str, encoding: str = "utf-8") -> str: + """Read a text resource bundled inside deeplabcut.gui.""" + return ASSETS_DIR.joinpath(*parts).read_text(encoding=encoding) + + +def get_assets_dir() -> Path: + """Get the path to the assets directory.""" + return Path(ASSETS_DIR) + + +def get_style_qss() -> str: + """Get the contents of the style.qss file.""" + return resource_text("style.qss") + + +def pixmap_from_resource(*parts: str) -> QPixmap: + pixmap = QPixmap() + data = resource_bytes(*parts) + + if not pixmap.loadFromData(data): + joined = "/".join(parts) + raise FileNotFoundError(f"Could not load GUI resource as QPixmap: {joined}") + + return pixmap + + +def icon_from_resource(*parts: str) -> QIcon: + return QIcon(pixmap_from_resource(*parts)) diff --git a/deeplabcut/gui/label_frames.py b/deeplabcut/gui/label_frames.py deleted file mode 100644 index 2b97397657..0000000000 --- a/deeplabcut/gui/label_frames.py +++ /dev/null @@ -1,218 +0,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 -import pydoc -import sys - -import wx - -from deeplabcut.generate_training_dataset import check_labels -from deeplabcut.gui import LOGO_PATH -from deeplabcut.utils import auxiliaryfunctions, skeleton -from pathlib import Path - - -def label_frames( - config, - multiple_individualsGUI=False, - imtypes=["*.png"], - config3d=None, - sourceCam=None, -): - """ - Manually label/annotate the extracted frames. Update the list of body parts you want to localize in the config.yaml file first. - - Parameter - ---------- - config : string - String containing the full path of the config file in the project. - - multiple_individualsGUI: bool, optional - If this is set to True, a user can label multiple individuals. Note for "multianimalproject=True" this is automatically used. - The default is ``False``; if provided it must be either ``True`` or ``False``. - - imtypes: list of imagetypes to look for in folder to be labeled. - By default only png images are considered. - - config3d: string, optional - String containing the full path of the config file in the 3D project. Include when epipolar lines would be helpful for labeling additional camera angles. - - sourceCam: string, optional - String containing the camera name from which to pull labeling data to generate epipolar lines. This must match the pattern in 'camera_names' in the 3D config file. - If no value is entered, data will be pulled from either cam1 or cam2 - - Example - -------- - Standard use case: - >>> deeplabcut.label_frames('/myawesomeproject/reaching4thestars/config.yaml') - - To label multiple individuals (without having a multiple individuals project); otherwise this GUI is loaded automatically - >>> deeplabcut.label_frames('/analysis/project/reaching-task/config.yaml',multiple_individualsGUI=True) - - To label other image types - >>> label_frames(config,multiple=False,imtypes=['*.jpg','*.jpeg']) - - To label with epipolar lines projected from labels in another camera angle #+++ - >>> label_frames(config, config3d='/analysis/project/reaching-task/reaching-task-3d/config.yaml', sourceCam='cam1') - -------- - - """ - startpath = os.getcwd() - wd = Path(config).resolve().parents[0] - os.chdir(str(wd)) - cfg = auxiliaryfunctions.read_config(config) - if cfg.get("multianimalproject", False) or multiple_individualsGUI: - from deeplabcut.gui import multiple_individuals_labeling_toolbox - - multiple_individuals_labeling_toolbox.show(config, config3d, sourceCam) - else: - from deeplabcut.gui import labeling_toolbox - - labeling_toolbox.show(config, config3d, sourceCam, imtypes=imtypes) - - os.chdir(startpath) - - -class Label_frames(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.method = "automatic" - self.config = cfg - # design the panel - sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 3. Label Frames") - sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - sizer.Add(icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5) - - line1 = wx.StaticLine(self) - sizer.Add(line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_BUTTON, self.select_config) - - self.help_button = wx.Button(self, label="Help") - sizer.Add(self.help_button, pos=(4, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.check = wx.Button(self, label="Check Labels!") - sizer.Add(self.check, pos=(5, 4), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.check.Bind(wx.EVT_BUTTON, self.check_labelF) - self.check.Enable(True) - - # self.build = wx.Button(self, label="Build skeleton") - # sizer.Add(self.build, pos=(4, 3), flag=wx.BOTTOM | wx.RIGHT, border=10) - # self.build.Bind(wx.EVT_BUTTON, self.build_skeleton) - # self.build.Enable(True) - - self.cfg = auxiliaryfunctions.read_config(self.config) - if self.cfg.get("multianimalproject", False): - - self.check = wx.Button(self, label="Check Labels Individuals") - sizer.Add(self.check, pos=(5, 3), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.check.Bind(wx.EVT_BUTTON, self.check_labelInd) - self.check.Enable(True) - - self.ok = wx.Button(self, label="Label Frames") - sizer.Add(self.ok, pos=(4, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.label_frames) - - self.reset = wx.Button(self, label="Reset") - sizer.Add( - self.reset, pos=(4, 1), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.reset.Bind(wx.EVT_BUTTON, self.reset_label_frames) - - sizer.AddGrowableCol(2) - - self.SetSizer(sizer) - sizer.Fit(self) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.label_frames" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def check_labelF(self, event): - dlg = wx.MessageDialog( - None, - "This will now plot the labeled frames afer you have finished labeling!", - ) - result = dlg.ShowModal() - check_labels(self.config, visualizeindividuals=False) - - def check_labelInd(self, event): - dlg = wx.MessageDialog( - None, - "This will now plot the labeled frames afer you have finished labeling!", - ) - result = dlg.ShowModal() - check_labels(self.config, visualizeindividuals=True) - - # def build_skeleton(self, event): - # skeleton.SkeletonBuilder(self.config) - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def label_frames(self, event): - label_frames(self.config) - - def reset_label_frames(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") diff --git a/deeplabcut/gui/labeling_toolbox.py b/deeplabcut/gui/labeling_toolbox.py deleted file mode 100755 index 221a84dfb7..0000000000 --- a/deeplabcut/gui/labeling_toolbox.py +++ /dev/null @@ -1,966 +0,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 argparse -import glob -import os -import os.path -from pathlib import Path - -import cv2 -import re -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import wx -import wx.lib.scrolledpanel as SP -from matplotlib.backends.backend_wxagg import ( - NavigationToolbar2WxAgg as NavigationToolbar, -) -from mpl_toolkits.axes_grid1 import make_axes_locatable - -from deeplabcut.gui import auxfun_drag -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import auxiliaryfunctions, auxiliaryfunctions_3d - - -class ImagePanel(BasePanel): - def __init__(self, parent, config, config3d, sourceCam, gui_size, **kwargs): - super(ImagePanel, self).__init__(parent, config, gui_size, **kwargs) - self.config = config - self.config3d = config3d - self.sourceCam = sourceCam - self.toolbar = None - - def retrieveData_and_computeEpLines(self, img, imNum): - - # load labeledPoints and fundamental Matrix - - if self.config3d is not None: - cfg_3d = auxiliaryfunctions.read_config(self.config3d) - cams = cfg_3d["camera_names"] - path_camera_matrix = auxiliaryfunctions_3d.Foldernames3Dproject(cfg_3d)[2] - path_stereo_file = os.path.join(path_camera_matrix, "stereo_params.pickle") - stereo_file = auxiliaryfunctions.read_pickle(path_stereo_file) - - for cam in cams: - if cam in img: - labelCam = cam - if self.sourceCam is None: - sourceCam = [ - otherCam for otherCam in cams if cam not in otherCam - ][ - 0 - ] # WHY? - else: - sourceCam = self.sourceCam - - sourceCamIdx = np.where(np.array(cams) == sourceCam)[0][0] - labelCamIdx = np.where(np.array(cams) == labelCam)[0][0] - - if sourceCamIdx < labelCamIdx: - camera_pair = cams[sourceCamIdx] + "-" + cams[labelCamIdx] - sourceCam_numInPair = 1 - else: - camera_pair = cams[labelCamIdx] + "-" + cams[sourceCamIdx] - sourceCam_numInPair = 2 - - fundMat = stereo_file[camera_pair]["F"] - sourceCam_path = os.path.split(img.replace(labelCam, sourceCam))[0] - - cfg = auxiliaryfunctions.read_config(self.config) - scorer = cfg["scorer"] - - try: - dataFrame = pd.read_hdf( - os.path.join(sourceCam_path, "CollectedData_" + scorer + ".h5") - ) - dataFrame.sort_index(inplace=True) - except IOError: - print( - "source camera images have not yet been labeled, or you have opened this folder in the wrong mode!" - ) - return None, None, None - - # Find offset terms for drawing epipolar Lines - # Get crop params for camera being labeled - foundEvent = 0 - eventSearch = re.compile(os.path.split(os.path.split(img)[0])[1]) - cropPattern = re.compile("[0-9]{1,4}") - with open(self.config, "rt") as config: - for line in config: - if foundEvent == 1: - crop_labelCam = np.int32(re.findall(cropPattern, line)) - break - if eventSearch.search(line) != None: - foundEvent = 1 - # Get crop params for other camera - foundEvent = 0 - eventSearch = re.compile(os.path.split(sourceCam_path)[1]) - cropPattern = re.compile("[0-9]{1,4}") - with open(self.config, "rt") as config: - for line in config: - if foundEvent == 1: - crop_sourceCam = np.int32(re.findall(cropPattern, line)) - break - if eventSearch.search(line) != None: - foundEvent = 1 - - labelCam_offsets = [crop_labelCam[0], crop_labelCam[2]] - sourceCam_offsets = [crop_sourceCam[0], crop_sourceCam[2]] - - sourceCam_pts = np.asarray(dataFrame, dtype=np.int32) - sourceCam_pts = sourceCam_pts.reshape( - (sourceCam_pts.shape[0], int(sourceCam_pts.shape[1] / 2), 2) - ) - sourceCam_pts = np.moveaxis(sourceCam_pts, [0, 1, 2], [1, 0, 2]) - sourceCam_pts[..., 0] = sourceCam_pts[..., 0] + sourceCam_offsets[0] - sourceCam_pts[..., 1] = sourceCam_pts[..., 1] + sourceCam_offsets[1] - - sourcePts = sourceCam_pts[:, imNum, :] - - epLines_source2label = cv2.computeCorrespondEpilines( - sourcePts, int(sourceCam_numInPair), fundMat - ) - epLines_source2label.reshape(-1, 3) - - return epLines_source2label, sourcePts, labelCam_offsets - - else: - return None, None, None - - def drawEpLines(self, drawImage, lines, sourcePts, offsets, colorIndex, cmap): - drawImage = cv2.cvtColor(drawImage, cv2.COLOR_BGR2RGB) - height, width, depth = drawImage.shape - labelNum = 0 - for line, pt, cIdx in zip(lines, sourcePts, colorIndex): - if pt[0] > -1000: - coeffs = line[0] - x0, y0 = map(int, [0 - offsets[0], -coeffs[2] / coeffs[1] - offsets[1]]) - x1, y1 = map( - int, - [ - width, - -(coeffs[2] + coeffs[0] * (width + offsets[0])) / coeffs[1] - - offsets[1], - ], - ) - cIdx = cIdx / 255 - color = cmap(cIdx, bytes=True)[:-1] - color = tuple([int(x) for x in color]) - drawImage = cv2.line(drawImage, (x0, y0), (x1, y1), color, 1) - - return drawImage - - def drawplot(self, img, img_name, itr, index, bodyparts, cmap, keep_view=False): - xlim = self.axes.get_xlim() - ylim = self.axes.get_ylim() - self.axes.clear() - - im = cv2.imread(img)[..., ::-1] - colorIndex = np.linspace(np.max(im), np.min(im), len(bodyparts)) - # draw epipolar lines - epLines, sourcePts, offsets = self.retrieveData_and_computeEpLines(img, itr) - if epLines is not None: - im = self.drawEpLines(im, epLines, sourcePts, offsets, colorIndex, cmap) - ax = self.axes.imshow(im, cmap=cmap) - self.orig_xlim = self.axes.get_xlim() - self.orig_ylim = self.axes.get_ylim() - divider = make_axes_locatable(self.axes) - cax = divider.append_axes("right", size="5%", pad=0.05) - cbar = self.figure.colorbar( - ax, cax=cax, spacing="proportional", ticks=colorIndex - ) - cbar.set_ticklabels(bodyparts[::-1]) - self.axes.set_title(str(str(itr) + "/" + str(len(index) - 1) + " " + img_name)) - if keep_view: - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) - if self.toolbar is None: - self.toolbar = NavigationToolbar(self.canvas) - return (self.figure, self.axes, self.canvas, self.toolbar) - - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - im = cv2.imread(img) - norm = mcolors.Normalize(vmin=0, vmax=np.max(im)) - ticks = np.linspace(0, np.max(im), len(bodyparts))[::-1] - return norm, ticks - - -class ScrollPanel(SP.ScrolledPanel): - def __init__(self, parent): - SP.ScrolledPanel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) - self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - self.Layout() - - def on_focus(self, event): - pass - - def addRadioButtons(self, bodyparts, fileIndex, markersize): - """ - Adds radio buttons for each bodypart on the right panel - """ - self.choiceBox = wx.BoxSizer(wx.VERTICAL) - choices = [l for l in bodyparts] - self.fieldradiobox = wx.RadioBox( - self, - label="Select a bodypart to label", - style=wx.RA_SPECIFY_ROWS, - choices=choices, - ) - self.slider = wx.Slider( - self, - -1, - markersize, - 1, - markersize * 3, - size=(250, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - self.slider.Enable(False) - self.checkBox = wx.CheckBox(self, id=wx.ID_ANY, label="Adjust marker size.") - self.choiceBox.Add(self.slider, 0, wx.ALL, 5) - self.choiceBox.Add(self.checkBox, 0, wx.ALL, 5) - self.choiceBox.Add(self.fieldradiobox, 0, wx.EXPAND | wx.ALL, 10) - self.SetSizerAndFit(self.choiceBox) - self.Layout() - return (self.choiceBox, self.fieldradiobox, self.slider, self.checkBox) - - def clearBoxer(self): - self.choiceBox.Clear(True) - - -class MainFrame(BaseFrame): - def __init__(self, parent, config, imtypes, config3d, sourceCam): - super(MainFrame, self).__init__( - "DeepLabCut2.0 - Labeling ToolBox", parent, imtypes - ) - - self.statusbar.SetStatusText( - "Looking for a folder to start labeling. Click 'Load frames' to begin." - ) - self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) - ################################################################################################################################################### - - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - - topSplitter = wx.SplitterWindow(self) - vSplitter = wx.SplitterWindow(topSplitter) - - self.image_panel = ImagePanel( - vSplitter, config, config3d, sourceCam, self.gui_size - ) - self.choice_panel = ScrollPanel(vSplitter) - vSplitter.SplitVertically( - self.image_panel, self.choice_panel, sashPosition=self.gui_size[0] * 0.8 - ) - vSplitter.SetSashGravity(1) - self.widget_panel = WidgetPanel(topSplitter) - topSplitter.SplitHorizontally( - vSplitter, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - self.load = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Load frames") - widgetsizer.Add(self.load, 1, wx.ALL, 15) - self.load.Bind(wx.EVT_BUTTON, self.browseDir) - - self.prev = wx.Button(self.widget_panel, id=wx.ID_ANY, label="<= 1: - curr_image = self.relativeimagenames[self.iter] - prev_image = self.relativeimagenames[self.iter - 1] - self.dataFrame.loc[curr_image] = self.dataFrame.loc[prev_image].values - img_name = Path(self.index[self.iter]).name - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.bodyparts, - self.colormap, - keep_view=self.view_locked, - ) - self.buttonCounter = MainFrame.plot(self, self.img) - - def activateSlider(self, event): - """ - Activates the slider to increase the markersize - """ - self.checkSlider = event.GetEventObject() - if self.checkSlider.GetValue(): - self.activate_slider = True - self.slider.Enable(True) - MainFrame.updateZoomPan(self) - else: - self.slider.Enable(False) - - def OnSliderScroll(self, event): - """ - Adjust marker size for plotting the annotations - """ - MainFrame.saveEachImage(self) - MainFrame.updateZoomPan(self) - self.buttonCounter = [] - self.markerSize = self.slider.GetValue() - img_name = Path(self.index[self.iter]).name - self.figure.delaxes(self.figure.axes[1]) - self.figure, self.axes, self.canvas, self.toolbar = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.bodyparts, - self.colormap, - keep_view=True, - ) - - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - self.buttonCounter = MainFrame.plot(self, self.img) - - def quitButton(self, event): - """ - Asks user for its inputs and then quits the GUI - """ - self.statusbar.SetStatusText("Qutting now!") - - nextFilemsg = wx.MessageBox( - "Do you want to label another data set?", - "Repeat?", - wx.YES_NO | wx.ICON_INFORMATION, - ) - if nextFilemsg == 2: - self.file = 1 - self.buttonCounter = [] - self.updatedCoords = [] - self.dataFrame = None - self.bodyparts = [] - self.new_labels = self.new_labels - self.axes.clear() - self.figure.delaxes(self.figure.axes[1]) - self.choiceBox.Clear(True) - MainFrame.updateZoomPan(self) - MainFrame.browseDir(self, event) - self.save.Enable(True) - else: - self.Destroy() - print( - "You can now check the labels, using 'check_labels' before proceeding. Then, you can use the function 'create_training_dataset' to create the training dataset." - ) - - def helpButton(self, event): - """ - Opens Instructions - """ - MainFrame.updateZoomPan(self) - wx.MessageBox( - "1. Select an individual and one of the body parts from the radio buttons to add a label (if necessary change config.yaml first to edit the label names). \n\n2. Right clicking on the image will add the selected label and the next available label will be selected from the radio button. \n The label will be marked as circle filled with a unique color (and individual ID a unique color on the rim).\n\n3. To change the marker size, mark the checkbox and move the slider, then uncheck the box. \n\n4. Hover your mouse over this newly added label to see its name. \n\n5. Use left click and drag to move the label position. \n\n6. Once you are happy with the position, right click to add the next available label. You can always reposition the old labels, if required. You can delete a label with the middle button mouse click (or click 'delete' key). \n\n7. Click Next/Previous to move to the next/previous image (or hot-key arrows left and right).\n User can also re-label a deletd point by going to a previous/next image then returning to the current iamge. \n NOTE: the user cannot add a label if the label is already present. \n \n8. You can click Cntrl+C to copy+paste labels from a previous image into the current image. \n\n9. When finished labeling all the images, click 'Save' to save all the labels as a .h5 file. \n\n10. Click OK to continue using the labeling GUI. For more tips and hotkeys: see docs!!", - "User instructions", - wx.OK | wx.ICON_INFORMATION, - ) - self.statusbar.SetStatusText("Help") - - def onButtonRelease(self, event): - if self.pan.GetValue(): - self.updateZoomPan() - self.statusbar.SetStatusText("Pan Off") - - def onClick(self, event): - """ - This function adds labels and auto advances to the next label. - """ - x1 = event.xdata - y1 = event.ydata - - if event.button == 3: - if self.rdb.GetSelection() in self.buttonCounter: - wx.MessageBox( - "%s is already annotated. \n Select another body part to annotate." - % (str(self.bodyparts[self.rdb.GetSelection()])), - "Error!", - wx.OK | wx.ICON_ERROR, - ) - else: - color = self.colormap( - self.norm(self.colorIndex[self.rdb.GetSelection()]) - ) - circle = [ - patches.Circle( - (x1, y1), radius=self.markerSize, fc=color, alpha=self.alpha - ) - ] - self.num.append(circle) - self.axes.add_patch(circle[0]) - self.dr = auxfun_drag.DraggablePoint( - circle[0], self.bodyparts[self.rdb.GetSelection()] - ) - self.dr.connect() - self.buttonCounter.append(self.rdb.GetSelection()) - self.dr.coords = [ - [ - x1, - y1, - self.bodyparts[self.rdb.GetSelection()], - self.rdb.GetSelection(), - ] - ] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - if self.rdb.GetSelection() < len(self.bodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() + 1) - self.figure.canvas.draw() - - self.canvas.mpl_disconnect(self.onClick) - self.canvas.mpl_disconnect(self.onButtonRelease) - - def nextLabel(self, event): - """ - This function is to create a hotkey to skip down on the radio button panel. - """ - if self.rdb.GetSelection() < len(self.bodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() + 1) - - def previousLabel(self, event): - """ - This function is to create a hotkey to skip up on the radio button panel. - """ - if self.rdb.GetSelection() < len(self.bodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() - 1) - - def browseDir(self, event): - """ - Show the DirDialog and ask the user to change the directory where machine labels are stored - """ - self.statusbar.SetStatusText("Looking for a folder to start labeling...") - cwd = os.path.join(os.getcwd(), "labeled-data") - dlg = wx.DirDialog( - self, - "Choose the directory where your extracted frames are saved:", - cwd, - style=wx.DD_DEFAULT_STYLE, - ) - if dlg.ShowModal() == wx.ID_OK: - self.dir = dlg.GetPath() - self.load.Enable(False) - self.next.Enable(True) - self.save.Enable(True) - else: - dlg.Destroy() - self.Close(True) - return - dlg.Destroy() - - # Enabling the zoom, pan and home buttons - self.zoom.Enable(True) - self.home.Enable(True) - self.pan.Enable(True) - self.lock.Enable(True) - - # Reading config file and its variables - self.cfg = auxiliaryfunctions.read_config(self.config_file) - self.scorer = self.cfg["scorer"] - self.bodyparts = self.cfg["bodyparts"] - self.videos = self.cfg["video_sets"].keys() - self.markerSize = self.cfg["dotsize"] - self.alpha = self.cfg["alphavalue"] - self.colormap = plt.get_cmap(self.cfg["colormap"]) - self.colormap = self.colormap.reversed() - self.project_path = self.cfg["project_path"] - - imlist = [] - for imtype in self.imtypes: - imlist.extend( - [ - fn - for fn in glob.glob(os.path.join(self.dir, imtype)) - if ("labeled.png" not in fn) - ] - ) - - if len(imlist) == 0: - print("No images found!!") - - self.index = np.sort(imlist) - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.relativeimagenames = [ - "labeled" + n.split("labeled")[1] for n in self.index - ] # [n.split(self.project_path+'/')[1] for n in self.index] - - # Reading the existing dataset,if already present - try: - self.dataFrame = pd.read_hdf( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".h5") - ) - self.dataFrame.sort_index(inplace=True) - self.prev.Enable(True) - - # Finds the first empty row in the dataframe and sets the iteration to that index - for idx, j in enumerate(self.dataFrame.index): - values = self.dataFrame.loc[j, :].values - if np.prod(np.isnan(values)) == 1: - self.iter = idx - break - else: - self.iter = 0 - - except: - a = np.empty((len(self.index), 2)) - a[:] = np.nan - for bodypart in self.bodyparts: - index = pd.MultiIndex.from_product( - [[self.scorer], [bodypart], ["x", "y"]], - names=["scorer", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=self.relativeimagenames) - self.dataFrame = pd.concat([self.dataFrame, frame], axis=1) - self.iter = 0 - - # Reading the image name - self.img = self.dataFrame.index[self.iter] - img_name = Path(self.img).name - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.bodyparts - ) - - # Checking for new frames and adding them to the existing dataframe - old_imgs = np.sort(list(self.dataFrame.index)) - self.newimages = list(set(self.relativeimagenames) - set(old_imgs)) - if not self.newimages: - pass - else: - print("Found new frames..") - # Create an empty dataframe with all the new images and then merge this to the existing dataframe. - self.df = None - a = np.empty((len(self.newimages), 2)) - a[:] = np.nan - for bodypart in self.bodyparts: - index = pd.MultiIndex.from_product( - [[self.scorer], [bodypart], ["x", "y"]], - names=["scorer", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=self.newimages) - self.df = pd.concat([self.df, frame], axis=1) - self.dataFrame = pd.concat([self.dataFrame, self.df], axis=0) - # Sort it by the index values - self.dataFrame.sort_index(inplace=True) - - # checks for unique bodyparts - if len(self.bodyparts) != len(set(self.bodyparts)): - print( - "Error - bodyparts must have unique labels! Please choose unique bodyparts in config.yaml file and try again. Quitting for now!" - ) - self.Close(True) - - # Extracting the list of new labels - oldBodyParts = self.dataFrame.columns.get_level_values(1) - _, idx = np.unique(oldBodyParts, return_index=True) - oldbodyparts2plot = list(oldBodyParts[np.sort(idx)]) - self.new_bodyparts = [x for x in self.bodyparts if x not in oldbodyparts2plot] - # Checking if user added a new label - if not self.new_bodyparts: # i.e. no new label - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, img_name, self.iter, self.index, self.bodyparts, self.colormap - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - - ( - self.choiceBox, - self.rdb, - self.slider, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.bodyparts, self.file, self.markerSize - ) - self.buttonCounter = MainFrame.plot(self, self.img) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - self.canvas.mpl_connect("button_release_event", self.onButtonRelease) - else: - dlg = wx.MessageDialog( - None, - "New label found in the config file. Do you want to see all the other labels?", - "New label found", - wx.YES_NO | wx.ICON_WARNING, - ) - result = dlg.ShowModal() - if result == wx.ID_NO: - self.bodyparts = self.new_bodyparts - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.bodyparts - ) - a = np.empty((len(self.index), 2)) - a[:] = np.nan - for bodypart in self.new_bodyparts: - index = pd.MultiIndex.from_product( - [[self.scorer], [bodypart], ["x", "y"]], - names=["scorer", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=self.relativeimagenames) - self.dataFrame = pd.concat([self.dataFrame, frame], axis=1) - - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, img_name, self.iter, self.index, self.bodyparts, self.colormap - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - - ( - self.choiceBox, - self.rdb, - self.slider, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.bodyparts, self.file, self.markerSize - ) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - self.canvas.mpl_connect("button_release_event", self.onButtonRelease) - self.buttonCounter = MainFrame.plot(self, self.img) - - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activateSlider) - self.slider.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - - def nextImage(self, event): - """ - Moves to next image - """ - # Checks for the last image and disables the Next button - if len(self.index) - self.iter == 1: - self.next.Enable(False) - return - self.prev.Enable(True) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.rdb.SetSelection(0) - self.file = 1 - # Refreshing the button counter - self.buttonCounter = [] - - MainFrame.saveEachImage(self) - self.iter = self.iter + 1 - - if len(self.index) >= self.iter: - self.updatedCoords = MainFrame.getLabels(self, self.iter) - self.img = self.index[self.iter] - img_name = Path(self.index[self.iter]).name - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.bodyparts, - self.colormap, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - - self.buttonCounter = MainFrame.plot(self, self.img) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - self.canvas.mpl_connect("button_release_event", self.onButtonRelease) - - def prevImage(self, event): - """ - Checks the previous Image and enables user to move the annotations. - """ - # Checks for the first image and disables the Previous button - if self.iter == 0: - self.prev.Enable(False) - return - else: - self.next.Enable(True) - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - MainFrame.saveEachImage(self) - - self.buttonCounter = [] - self.iter = self.iter - 1 - - self.rdb.SetSelection(0) - self.img = self.index[self.iter] - img_name = Path(self.index[self.iter]).name - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - self.figure, self.axes, self.canvas, self.toolbar = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.bodyparts, - self.colormap, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - - self.buttonCounter = MainFrame.plot(self, self.img) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - self.canvas.mpl_connect("button_release_event", self.onButtonRelease) - MainFrame.saveEachImage(self) - - def getLabels(self, img_index): - """ - Returns a list of x and y labels of the corresponding image index - """ - self.previous_image_points = [] - for bpindex, bp in enumerate(self.bodyparts): - image_points = [ - [ - self.dataFrame[self.scorer][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][bp]["y"].values[self.iter], - bp, - bpindex, - ] - ] - self.previous_image_points.append(image_points) - return self.previous_image_points - - def plot(self, img): - """ - Plots and call auxfun_drag class for moving and removing points. - """ - self.drs = [] - self.updatedCoords = [] - for bpindex, bp in enumerate(self.bodyparts): - color = self.colormap(self.norm(self.colorIndex[bpindex])) - self.points = [ - self.dataFrame[self.scorer][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][bp]["y"].values[self.iter], - ] - circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - alpha=self.alpha, - ) - ] - self.axes.add_patch(circle[0]) - self.dr = auxfun_drag.DraggablePoint(circle[0], self.bodyparts[bpindex]) - self.dr.connect() - self.dr.coords = MainFrame.getLabels(self, self.iter)[bpindex] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - if not np.isnan(self.points)[0]: - self.buttonCounter.append(bpindex) - self.figure.canvas.draw() - - return self.buttonCounter - - def saveEachImage(self): - """ - Saves data for each image - """ - for idx, bp in enumerate(self.updatedCoords): - self.dataFrame.loc[self.relativeimagenames[self.iter]][ - self.scorer, bp[0][-2], "x" - ] = bp[-1][0] - self.dataFrame.loc[self.relativeimagenames[self.iter]][ - self.scorer, bp[0][-2], "y" - ] = bp[-1][1] - - def saveDataSet(self, event): - """ - Saves the final dataframe - """ - self.statusbar.SetStatusText("File saved") - MainFrame.saveEachImage(self) - MainFrame.updateZoomPan(self) - - # Windows compatible - self.dataFrame.sort_index(inplace=True) - self.dataFrame = self.dataFrame.reindex( - self.cfg["bodyparts"], - axis=1, - level=self.dataFrame.columns.names.index("bodyparts"), - ) - self.dataFrame.to_csv( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".csv") - ) - self.dataFrame.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".h5"), - "df_with_missing", - format="table", - mode="w", - ) - - def onChecked(self, event): - self.cb = event.GetEventObject() - if self.cb.GetValue(): - self.slider.Enable(True) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - self.canvas.mpl_connect("button_release_event", self.onButtonRelease) - else: - self.slider.Enable(False) - - -def show(config, config3d, sourceCam, imtypes=["*.png"]): - app = wx.App() - frame = MainFrame(None, config, imtypes, config3d, sourceCam).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config") - parser.add_argument("config3d") - parser.add_argument("sourceCam") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/launch_script.py b/deeplabcut/gui/launch_script.py index 364a5ddeee..e2e70a7230 100644 --- a/deeplabcut/gui/launch_script.py +++ b/deeplabcut/gui/launch_script.py @@ -1,3 +1,13 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# """ DeepLabCut2.0 Toolbox (deeplabcut.org) © A. & M. Mathis Labs @@ -8,39 +18,57 @@ Licensed under GNU Lesser General Public License v3.0 """ -import os -import wx - -from deeplabcut.gui.create_new_project import Create_new_project -from deeplabcut.gui.welcome import Welcome -from deeplabcut.gui.widgets import BaseFrame -from deeplabcut.utils import auxiliaryfunctions - - -class MainFrame(BaseFrame): - def __init__(self): - super(MainFrame, self).__init__("DeepLabCut") - self.statusbar.SetStatusText("www.deeplabcut.org") - dlcparent_path = auxiliaryfunctions.get_deeplabcut_path() - media_path = os.path.join(dlcparent_path, "gui", "media") - logo = os.path.join(media_path, "logo.png") - self.SetIcon(wx.Icon(logo)) - # Here we create a panel and a notebook on the panel - self.panel = wx.Panel(self) - self.nb = wx.Notebook(self.panel) - # create the page windows as children of the notebook and add the pages to the notebook with the label to show on the tab - page1 = Welcome(self.nb, self.gui_size) - self.nb.AddPage(page1, "Welcome") - - page2 = Create_new_project(self.nb, self.gui_size) - self.nb.AddPage(page2, "Manage Project") - - self.sizer = wx.BoxSizer() - self.sizer.Add(self.nb, 1, wx.EXPAND) - self.panel.SetSizer(self.sizer) + +import logging +import sys + +import PySide6.QtWidgets as QtWidgets +import qdarkstyle +from PySide6.QtCore import Qt + +from deeplabcut.gui.gui_assets import get_style_qss, icon_from_resource, pixmap_from_resource + +logger = logging.getLogger(__name__) def launch_dlc(): - app = wx.App() - frame = MainFrame().Show() - app.MainLoop() + app = QtWidgets.QApplication(sys.argv) + app.setWindowIcon(icon_from_resource("logo.png")) + screen_size = app.screens()[0].size() + pixmap = pixmap_from_resource("welcome.png").scaledToWidth(int(0.7 * screen_size.width()), Qt.SmoothTransformation) + splash = QtWidgets.QSplashScreen(pixmap) + splash.show() + + app.setStyleSheet(get_style_qss()) # this gets overridden immediately? + try: + dark_stylesheet = qdarkstyle.load_stylesheet_pyside6() + except Exception as e: + logger.warning(f"Could not load qdarkstyle stylesheet for PySide6: {e}. Falling back to PySide2 stylesheet.") + dark_stylesheet = qdarkstyle.load_stylesheet_pyside2() + app.setStyleSheet(dark_stylesheet) + + # Set up a logger and add an stdout handler. + # A single logger can have many handlers: + # https://docs.python.org/3/howto/logging.html#handler-basic + # TODO Dump to log file instead + # logger = logging.getLogger("GUI") + # logger.setLevel(logging.DEBUG) + # handler = logging.StreamHandler(stream=sys.stdout) + # handler.setLevel(logging.DEBUG) + # formatter = logging.Formatter( + # "%(asctime)s - %(name)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S" + # ) + # handler.setFormatter(formatter) + # logger.addHandler(handler) + + from deeplabcut.gui.window import MainWindow + + window = MainWindow(app) + window.receiver.start() + window.showMaximized() + splash.finish(window) + sys.exit(app.exec_()) + + +if __name__ == "__main__": + launch_dlc() diff --git a/deeplabcut/gui/load_project.py b/deeplabcut/gui/load_project.py deleted file mode 100644 index 10210891f9..0000000000 --- a/deeplabcut/gui/load_project.py +++ /dev/null @@ -1,102 +0,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 sys - -import wx - - -class Load_project(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.config = cfg - # design the panel - self.sizer = wx.GridBagSizer(10, 15) - - text = wx.StaticText(self, label="DeepLabCut Load project") - self.sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - # icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(logo)) - # self.sizer.Add(icon, pos=(0, 4), flag=wx.TOP|wx.RIGHT|wx.ALIGN_RIGHT,border=5) - - line1 = wx.StaticLine(self) - self.sizer.Add( - line1, pos=(1, 0), span=(1, 15), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - self.cfg = wx.StaticText(self, label="Select the config file") - self.sizer.Add(self.cfg, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - - if self.config is None: - self.config = "Please select the config file" - - self.sel_config.SetPath(self.config) - - self.sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 15), flag=wx.TOP | wx.EXPAND, border=0 - ) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - button3 = wx.Button(self, label="Help") - self.sizer.Add(button3, pos=(4, 0), flag=wx.LEFT, border=10) - - self.ok = wx.Button(self, label="Ok") - self.sizer.Add(self.ok, pos=(4, 5)) - self.ok.Bind(wx.EVT_BUTTON, self.load_project) - - self.cancel = wx.Button(self, label="Reset") - self.sizer.Add( - self.cancel, pos=(4, 3), span=(1, 1), flag=wx.BOTTOM | wx.RIGHT, border=10 - ) - self.cancel.Bind(wx.EVT_BUTTON, self.cancel_load_project) - - self.sizer.AddGrowableCol(2) - - self.SetSizer(self.sizer) - self.sizer.Fit(self) - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def load_project(self, event): - print(self.config) - - def cancel_load_project(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") diff --git a/deeplabcut/gui/media/__init__.py b/deeplabcut/gui/media/__init__.py index e69de29bb2..2dd1b06028 100644 --- a/deeplabcut/gui/media/__init__.py +++ b/deeplabcut/gui/media/__init__.py @@ -0,0 +1,10 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# diff --git a/deeplabcut/gui/media/dlc-pt.png b/deeplabcut/gui/media/dlc-pt.png new file mode 100644 index 0000000000..d0ac99c187 Binary files /dev/null and b/deeplabcut/gui/media/dlc-pt.png differ diff --git a/deeplabcut/gui/media/dlc-tf.png b/deeplabcut/gui/media/dlc-tf.png new file mode 100644 index 0000000000..79d06f0528 Binary files /dev/null and b/deeplabcut/gui/media/dlc-tf.png differ diff --git a/deeplabcut/gui/multiple_individuals_labeling_toolbox.py b/deeplabcut/gui/multiple_individuals_labeling_toolbox.py deleted file mode 100755 index 4a54380312..0000000000 --- a/deeplabcut/gui/multiple_individuals_labeling_toolbox.py +++ /dev/null @@ -1,1320 +0,0 @@ -""" -DeepLabCut2.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 -""" - -import argparse -import glob -import os -import os.path -from pathlib import Path - -import re -import cv2 -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import wx -import wx.lib.scrolledpanel as SP -from matplotlib.backends.backend_wxagg import ( - NavigationToolbar2WxAgg as NavigationToolbar, -) -from mpl_toolkits.axes_grid1 import make_axes_locatable - -from deeplabcut.gui import auxfun_drag -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import ( - auxiliaryfunctions, - auxfun_multianimal, - auxiliaryfunctions_3d, -) - -# ########################################################################### -# Class for GUI MainFrame -# ########################################################################### -class ImagePanel(BasePanel): - def __init__(self, parent, config, config3d, sourceCam, gui_size, **kwargs): - super(ImagePanel, self).__init__(parent, config, gui_size, **kwargs) - self.config = config - self.cfg = auxiliaryfunctions.read_config(self.config) - self.config3d = config3d - self.sourceCam = sourceCam - self.toolbar = None - - def retrieveData_and_computeEpLines(self, img, imNum): - - # load labeledPoints and fundamental Matrix - - if self.config3d is not None: - cfg_3d = auxiliaryfunctions.read_config(self.config3d) - cams = cfg_3d["camera_names"] - path_camera_matrix = auxiliaryfunctions_3d.Foldernames3Dproject(cfg_3d)[2] - path_stereo_file = os.path.join(path_camera_matrix, "stereo_params.pickle") - stereo_file = auxiliaryfunctions.read_pickle(path_stereo_file) - - for cam in cams: - if cam in img: - labelCam = cam - if self.sourceCam is None: - sourceCam = [ - otherCam for otherCam in cams if cam not in otherCam - ][0] - else: - sourceCam = self.sourceCam - - sourceCamIdx = np.where(np.array(cams) == sourceCam)[0][0] - labelCamIdx = np.where(np.array(cams) == labelCam)[0][0] - if sourceCamIdx < labelCamIdx: - camera_pair = cams[sourceCamIdx] + "-" + cams[labelCamIdx] - sourceCam_numInPair = 1 - else: - camera_pair = cams[labelCamIdx] + "-" + cams[sourceCamIdx] - sourceCam_numInPair = 2 - - fundMat = stereo_file[camera_pair]["F"] - sourceCam_path = os.path.split(img.replace(labelCam, sourceCam))[0] - - cfg = auxiliaryfunctions.read_config(self.config) - scorer = cfg["scorer"] - - try: - dataFrame = pd.read_hdf( - os.path.join(sourceCam_path, "CollectedData_" + scorer + ".h5") - ) - dataFrame.sort_index(inplace=True) - except IOError: - print( - "source camera images have not yet been labeled, or you have opened this folder in the wrong mode!" - ) - return None, None, None - - # Find offset terms for drawing epipolar Lines - # Get crop params for camera being labeled - foundEvent = 0 - eventSearch = re.compile(os.path.split(os.path.split(img)[0])[1]) - cropPattern = re.compile("[0-9]{1,4}") - with open(self.config, "rt") as config: - for line in config: - if foundEvent == 1: - crop_labelCam = np.int32(re.findall(cropPattern, line)) - break - if eventSearch.search(line) != None: - foundEvent = 1 - # Get crop params for other camera - foundEvent = 0 - eventSearch = re.compile(os.path.split(sourceCam_path)[1]) - cropPattern = re.compile("[0-9]{1,4}") - with open(self.config, "rt") as config: - for line in config: - if foundEvent == 1: - crop_sourceCam = np.int32(re.findall(cropPattern, line)) - break - if eventSearch.search(line) != None: - foundEvent = 1 - - labelCam_offsets = [crop_labelCam[0], crop_labelCam[2]] - sourceCam_offsets = [crop_sourceCam[0], crop_sourceCam[2]] - - sourceCam_pts = np.asarray(dataFrame, dtype=np.int32) - sourceCam_pts = sourceCam_pts.reshape( - (sourceCam_pts.shape[0], int(sourceCam_pts.shape[1] / 2), 2) - ) - sourceCam_pts = np.moveaxis(sourceCam_pts, [0, 1, 2], [1, 0, 2]) - sourceCam_pts[..., 0] = sourceCam_pts[..., 0] + sourceCam_offsets[0] - sourceCam_pts[..., 1] = sourceCam_pts[..., 1] + sourceCam_offsets[1] - - sourcePts = sourceCam_pts[:, imNum, :] - - epLines_source2label = cv2.computeCorrespondEpilines( - sourcePts, int(sourceCam_numInPair), fundMat - ) - epLines_source2label.reshape(-1, 3) - - return epLines_source2label, sourcePts, labelCam_offsets - - else: - return None, None, None - - def drawEpLines(self, drawImage, lines, sourcePts, offsets, colorIndex, cmap): - drawImage = cv2.cvtColor(drawImage, cv2.COLOR_BGR2RGB) - height, width, depth = drawImage.shape - for line, pt, cIdx in zip(lines, sourcePts, colorIndex): - if pt[0] > -1000: - coeffs = line[0] - x0, y0 = map(int, [0 - offsets[0], -coeffs[2] / coeffs[1] - offsets[1]]) - x1, y1 = map( - int, - [ - width, - -(coeffs[2] + coeffs[0] * (width + offsets[0])) / coeffs[1] - - offsets[1], - ], - ) - cIdx = cIdx / 255 - color = cmap(cIdx, bytes=True)[:-1] - color = tuple([int(x) for x in color]) - drawImage = cv2.line(drawImage, (x0, y0), (x1, y1), color, 1) - return drawImage - - def drawplot(self, img, img_name, itr, index, bodyparts, cmap, keep_view=False): - individuals = self.cfg["individuals"] - xlim = self.axes.get_xlim() - ylim = self.axes.get_ylim() - self.axes.clear() - # im = cv2.imread(img) - # convert the image to RGB as you are showing the image with matplotlib - im = cv2.imread(img)[..., ::-1] - colorIndex = [] - for indiv in range(len(individuals)): - colorIndex.extend(np.linspace(np.max(im), np.min(im), len(bodyparts))) - colorIndex = np.array(colorIndex) - # draw epipolar lines - epLines, sourcePts, offsets = self.retrieveData_and_computeEpLines(img, itr) - if epLines is not None: - im = self.drawEpLines(im, epLines, sourcePts, offsets, colorIndex, cmap) - - ax = self.axes.imshow(im, cmap=cmap) - self.orig_xlim = self.axes.get_xlim() - self.orig_ylim = self.axes.get_ylim() - # divider = make_axes_locatable(self.axes) - # colorIndex = np.linspace(np.min(im),np.max(im),len(bodyparts)) - # cax = divider.append_axes("right", size="5%", pad=0.05) - # cbar = self.figure.colorbar(ax, cax=cax,spacing='proportional', ticks=colorIndex) - # cbar.set_ticklabels(bodyparts[::-1]) - self.axes.set_title(str(str(itr) + "/" + str(len(index) - 1) + " " + img_name)) - # self.figure.canvas.draw() - if keep_view: - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) - if self.toolbar is None: - self.toolbar = NavigationToolbar(self.canvas) - return (self.figure, self.axes, self.canvas, self.toolbar, ax) - - def addcolorbar(self, img, ax, itr, bodyparts, cmap): - im = cv2.imread(img) - divider = make_axes_locatable(self.axes) - colorIndex = np.linspace(np.min(im), np.max(im), len(bodyparts)) - cax = divider.append_axes("right", size="5%", pad=0.05) - cbar = self.figure.colorbar( - ax, cax=cax, spacing="proportional", ticks=colorIndex - ) - cbar.set_ticklabels(bodyparts[::-1]) - self.figure.canvas.draw() - if self.toolbar is None: - self.toolbar = NavigationToolbar(self.canvas) - return (self.figure, self.axes, self.canvas, self.toolbar) - - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - im = cv2.imread(img) - norm = mcolors.Normalize(vmin=0, vmax=np.max(im)) - ticks = np.linspace(0, np.max(im), len(bodyparts))[::-1] - return norm, ticks - - -class ScrollPanel(SP.ScrolledPanel): - def __init__(self, parent): - SP.ScrolledPanel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) - self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - self.Layout() - - def on_focus(self, event): - pass - - def addRadioButtons(self, bodyparts, individual_names, fileIndex, markersize): - """ - Adds radio buttons for each bodypart on the right panel - """ - self.choiceBox = wx.BoxSizer(wx.VERTICAL) - choices = [l for l in bodyparts] - self.fieldradiobox = wx.RadioBox( - self, - label="Select a bodypart to label", - majorDimension=3, - style=wx.RA_SPECIFY_COLS, - choices=choices, - ) - self.change_marker = wx.Slider( - self, - -1, - markersize, - 1, - markersize * 3, - size=(250, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - self.change_marker.Enable(False) - names = [k for k in individual_names] - self.individualradiobox = wx.RadioBox( - self, - label="Select an individual", - majorDimension=3, - style=wx.RA_SPECIFY_COLS, - choices=names, - ) - - self.checkBox = wx.CheckBox(self, id=wx.ID_ANY, label="Adjust marker size") - self.choiceBox.Add(self.change_marker, 0, wx.ALL, 5) - self.choiceBox.Add(self.checkBox, 0, wx.ALL, 5) - self.choiceBox.Add(self.individualradiobox, 0, wx.EXPAND | wx.ALL, 10) - - self.choiceBox.Add(self.fieldradiobox, 0, wx.EXPAND | wx.ALL, 10) - self.SetSizerAndFit(self.choiceBox) - self.Layout() - return ( - self.choiceBox, - self.individualradiobox, - self.fieldradiobox, - self.change_marker, - self.checkBox, - ) - - def clearBoxer(self): - self.choiceBox.Clear(True) - - -class MainFrame(BaseFrame): - def __init__(self, parent, config, config3d, sourceCam): - super(MainFrame, self).__init__( - "DeepLabCut 2.2 - Multiple Individuals Labeling", parent - ) - - self.statusbar.SetStatusText( - "Looking for a folder to start labeling. Click 'Load frames' to begin." - ) - self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) - - ################################################################################################################################################### - - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - - topSplitter = wx.SplitterWindow(self) - vSplitter = wx.SplitterWindow(topSplitter) - - self.image_panel = ImagePanel( - vSplitter, config, config3d, sourceCam, self.gui_size - ) - self.choice_panel = ScrollPanel(vSplitter) - - vSplitter.SplitVertically( - self.image_panel, self.choice_panel, sashPosition=self.gui_size[0] * 0.8 - ) - vSplitter.SetSashGravity(1) - self.widget_panel = WidgetPanel(topSplitter) - topSplitter.SplitHorizontally( - vSplitter, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - self.load = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Load frames") - widgetsizer.Add(self.load, 1, wx.ALL, 15) - self.load.Bind(wx.EVT_BUTTON, self.browseDir) - - self.prev = wx.Button(self.widget_panel, id=wx.ID_ANY, label="<= 1: - curr_individual = self.individualrdb.GetStringSelection() - curr_image = self.relativeimagenames[self.iter] - prev_image = self.relativeimagenames[self.iter - 1] - idx = pd.IndexSlice - self.dataFrame.loc[ - curr_image, idx[:, curr_individual] - ] = self.dataFrame.loc[prev_image, idx[:, curr_individual]].values - img_name = Path(self.index[self.iter]).name - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=self.view_locked, - ) - if curr_individual == "single": - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.uniquebodyparts - ) - self.buttonCounter = MainFrame.plot(self, self.img) - else: - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.multibodyparts - ) - self.buttonCounter = MainFrame.plot(self, self.img) - - def activateSlider(self, event): - """ - Activates the slider to increase the markersize - """ - self.checkSlider = event.GetEventObject() - if self.checkSlider.GetValue(): - self.activate_slider = True - self.change_marker_size.Enable(True) - MainFrame.updateZoomPan(self) - else: - self.change_marker_size.Enable(False) - - def OnSliderScroll(self, event): - """ - Adjust marker size for plotting the annotations - """ - MainFrame.saveEachImage(self) - MainFrame.updateZoomPan(self) - self.updatedCoords = [] - self.markerSize = self.change_marker_size.GetValue() - self.edgewidth = self.markerSize // 3 - img_name = Path(self.index[self.iter]).name - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=True, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - self.buttonCounter = MainFrame.plot(self, self.img) - - def quitButton(self, event): - """ - Asks user for its inputs and then quits the GUI - """ - # MainFrame.saveDataSet(self, event) - self.statusbar.SetStatusText("Quitting now!") - - nextFilemsg = wx.MessageBox( - "Do you want to label another data set?", - "Repeat?", - wx.YES_NO | wx.ICON_INFORMATION, - ) - if nextFilemsg == 2: - self.file = 1 - self.buttonCounter = {i: [] for i in self.individual_names} - self.updatedCoords = [] - self.dataFrame = None - self.multibodyparts = [] - self.new_labels = self.new_labels - self.axes.clear() - self.figure.delaxes(self.figure.axes[1]) - self.choiceBox.Clear(True) - MainFrame.updateZoomPan(self) - MainFrame.browseDir(self, event) - else: - self.Destroy() - print( - "You can now check the labels, using 'check_labels' before proceeding. Then, you can use the function 'create_training_dataset' to create the training dataset." - ) - - def helpButton(self, event): - """ - Opens Instructions - """ - MainFrame.updateZoomPan(self) - wx.MessageBox( - "1. Select an individual and one of the body parts from the radio buttons to add a label (if necessary change config.yaml first to edit the label names). \n\n2. Right clicking on the image will add the selected label and the next available label will be selected from the radio button. \n The label will be marked as circle filled with a unique color (and individual ID a unique color on the rim).\n\n3. To change the marker size, mark the checkbox and move the slider, then uncheck the box. \n\n4. Hover your mouse over this newly added label to see its name. \n\n5. Use left click and drag to move the label position. \n\n6. Once you are happy with the position, right click to add the next available label. You can always reposition the old labels, if required. You can delete a label with the middle button mouse click (or click 'delete' key). \n\n7. Click Next/Previous to move to the next/previous image (or hot-key arrows left and right).\n User can also re-label a deletd point by going to a previous/next image then returning to the current iamge. \n NOTE: the user cannot add a label if the label is already present. \n \n8. You can click Cntrl+C to copy+paste labels from a previous image into the current image. For maDLC, you do this for each individual. \n\n9. When finished labeling all the images, click 'Save' to save all the labels as a .h5 file. \n\n10. Click OK to continue using the labeling GUI. For more tips and hotkeys: see docs!!", - "User instructions", - wx.OK | wx.ICON_INFORMATION, - ) - self.statusbar.SetStatusText("Help") - - def onButtonRelease(self, event): - if self.pan.GetValue(): - self.updateZoomPan() - self.statusbar.SetStatusText("Pan Off") - - def onClick(self, event): - """ - This function adds labels and auto advances to the next label. - """ - x1 = event.xdata - y1 = event.ydata - if event.button == 3: - num_indiv = self.individualrdb.GetSelection() - indiv = self.individual_names[num_indiv] - idcolor = self.idmap(num_indiv) - if self.individualrdb.GetStringSelection() == "single": - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.uniquebodyparts - ) - if ( - self.uniquebodyparts[self.rdb.GetSelection()] - in self.buttonCounter[indiv] - ): - wx.MessageBox( - "%s is already annotated for %s. \n Select another body part to annotate." - % ( - str(self.uniquebodyparts[self.rdb.GetSelection()]), - str( - self.individual_names[self.individualrdb.GetSelection()] - ), - ), - "Error!", - wx.OK | wx.ICON_ERROR, - ) - else: - color = self.colormap( - self.norm(self.colorIndex[self.rdb.GetSelection()]) - ) - circle = [ - patches.Circle( - (x1, y1), - radius=self.markerSize, - fc=color, - ec=idcolor, - lw=self.edgewidth, - alpha=self.alpha, - ) - ] - self.num.append(circle) - self.axes.add_patch(circle[0]) - self.dr = auxfun_drag.DraggablePoint( - circle[0], - self.uniquebodyparts[self.rdb.GetSelection()], - individual_names=indiv, - ) - self.dr.connect() - self.buttonCounter[indiv].append( - self.uniquebodyparts[self.rdb.GetSelection()] - ) - self.dr.coords = [ - [x1, y1, indiv, self.uniquebodyparts[self.rdb.GetSelection()]] - ] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - - if self.rdb.GetSelection() < len(self.uniquebodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() + 1) - else: - self.rdb.SetSelection(0) - if ( - self.individualrdb.GetSelection() - < len(self.individual_names) - 1 - ): - self.individualrdb.SetSelection( - self.individualrdb.GetSelection() + 1 - ) - MainFrame.select_individual(self, event) - - else: - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.multibodyparts - ) - if ( - self.multibodyparts[self.rdb.GetSelection()] - in self.buttonCounter[indiv] - ): - wx.MessageBox( - "%s is already annotated for %s. \n Select another body part to annotate." - % ( - str(self.multibodyparts[self.rdb.GetSelection()]), - str( - self.individual_names[self.individualrdb.GetSelection()] - ), - ), - "Error!", - wx.OK | wx.ICON_ERROR, - ) - else: - color = self.colormap( - self.norm(self.colorIndex[self.rdb.GetSelection()]) - ) - circle = [ - patches.Circle( - (x1, y1), - radius=self.markerSize, - fc=color, - ec=idcolor, - lw=self.edgewidth, - alpha=self.alpha, - ) - ] - self.num.append(circle) - self.axes.add_patch(circle[0]) - self.dr = auxfun_drag.DraggablePoint( - circle[0], - self.multibodyparts[self.rdb.GetSelection()], - individual_names=indiv, - ) - self.dr.connect() - self.buttonCounter[indiv].append( - self.multibodyparts[self.rdb.GetSelection()] - ) - self.dr.coords = [ - [x1, y1, indiv, self.multibodyparts[self.rdb.GetSelection()]] - ] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - - if self.rdb.GetSelection() < len(self.multibodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() + 1) - else: - self.rdb.SetSelection(0) - if ( - self.individualrdb.GetSelection() - < len(self.individual_names) - 1 - ): - self.individualrdb.SetSelection( - self.individualrdb.GetSelection() + 1 - ) - MainFrame.select_individual(self, event) - self.canvas.mpl_disconnect(self.onClick) - - def nextLabel(self, event): - """ - This function is to create a hotkey to skip down on the radio button panel. - """ - if self.rdb.GetSelection() < len(self.multibodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() + 1) - - def previousLabel(self, event): - """ - This function is to create a hotkey to skip up on the radio button panel. - """ - if self.rdb.GetSelection() < len(self.multibodyparts) - 1: - self.rdb.SetSelection(self.rdb.GetSelection() - 1) - - def browseDir(self, event): - """ - Show the DirDialog and ask the user to change the directory where machine labels are stored - """ - self.statusbar.SetStatusText("Looking for a folder to start labeling...") - cwd = os.path.join(os.getcwd(), "labeled-data") - dlg = wx.DirDialog( - self, - "Choose the directory where your extracted frames are saved:", - cwd, - style=wx.DD_DEFAULT_STYLE, - ) - if dlg.ShowModal() == wx.ID_OK: - self.dir = dlg.GetPath() - self.load.Enable(False) - self.next.Enable(True) - self.save.Enable(True) - else: - dlg.Destroy() - self.Close(True) - return - dlg.Destroy() - - # Enabling the zoom, pan and home buttons - self.zoom.Enable(True) - self.home.Enable(True) - self.pan.Enable(True) - self.lock.Enable(True) - - # Reading config file and its variables - self.cfg = auxiliaryfunctions.read_config(self.config_file) - self.scorer = self.cfg["scorer"] - ( - individuals, - uniquebodyparts, - multianimalbodyparts, - ) = auxfun_multianimal.extractindividualsandbodyparts(self.cfg) - - self.multibodyparts = multianimalbodyparts - # checks for unique bodyparts - if len(self.multibodyparts) != len(set(self.multibodyparts)): - print( - "Error - bodyparts must have unique labels! Please choose unique bodyparts in config.yaml file and try again. Quitting for now!" - ) - self.Close(True) - - self.uniquebodyparts = uniquebodyparts - self.individual_names = individuals - - self.videos = self.cfg["video_sets"].keys() - self.markerSize = self.cfg["dotsize"] - self.edgewidth = self.markerSize // 3 - self.alpha = self.cfg["alphavalue"] - self.colormap = plt.get_cmap(self.cfg["colormap"]) - self.colormap = self.colormap.reversed() - self.idmap = plt.cm.get_cmap("Set1", len(individuals)) - self.project_path = self.cfg["project_path"] - - if not self.uniquebodyparts: - self.are_unique_bodyparts_present = False - - self.buttonCounter = {i: [] for i in self.individual_names} - self.index = np.sort( - [ - fn - for fn in glob.glob(os.path.join(self.dir, "*.png")) - if ("labeled.png" not in fn) - ] - ) - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.relativeimagenames = [ - "labeled" + n.split("labeled")[1] for n in self.index - ] # [n.split(self.project_path+'/')[1] for n in self.index] - - # Reading the existing dataset,if already present - try: - self.dataFrame = pd.read_hdf( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".h5") - ) - # Handle data previously labeled on a different platform - sep = "/" if "/" in self.dataFrame.index[0] else "\\" - if sep != os.path.sep: - self.dataFrame.index = self.dataFrame.index.str.replace( - sep, os.path.sep - ) - self.dataFrame.sort_index(inplace=True) - self.prev.Enable(True) - # Finds the first empty row in the dataframe and sets the iteration to that index - self.iter = np.argmax(np.isnan(self.dataFrame.values).all(axis=1)) - except FileNotFoundError: - # Create an empty data frame - self.dataFrame = MainFrame.create_dataframe( - self, - self.dataFrame, - self.relativeimagenames, - self.individual_names, - self.uniquebodyparts, - self.multibodyparts, - ) - self.iter = 0 - - # Cache original bodyparts - self._old_multi = ( - self.dataFrame.xs(self.individual_names[0], axis=1, level="individuals") - .columns.get_level_values("bodyparts") - .unique() - .to_list() - ) - self._old_unique = ( - self.dataFrame.loc[ - :, self.dataFrame.columns.get_level_values("individuals") == "single" - ] - .columns.get_level_values("bodyparts") - .unique() - .to_list() - ) - - # Reading the image name - self.img = self.index[self.iter] - img_name = Path(self.index[self.iter]).name - - # Checking for new frames and adding them to the existing dataframe - old_imgs = np.sort(list(self.dataFrame.index)) - self.newimages = list(set(self.relativeimagenames) - set(old_imgs)) - if self.newimages: - print("Found new frames..") - # Create an empty dataframe with all the new images and then merge this to the existing dataframe. - self.df = MainFrame.create_dataframe( - self, - None, - self.newimages, - self.individual_names, - self.uniquebodyparts, - self.multibodyparts, - ) - self.dataFrame = pd.concat([self.dataFrame, self.df], axis=0) - self.dataFrame.sort_index(inplace=True) - # Rearrange bodypart columns in config order - bodyparts = self.multibodyparts + self.uniquebodyparts - self.dataFrame.reindex( - bodyparts, axis=1, level=self.dataFrame.columns.names.index("bodyparts") - ) - # Test whether there are missing frames and superfluous data - if len(old_imgs) > len(self.relativeimagenames): - missing_frames = set(old_imgs).difference(self.relativeimagenames) - self.dataFrame.drop(missing_frames, inplace=True) - - # Check whether new labels were added - self.new_multi = [x for x in self.multibodyparts if x not in self._old_multi] - self.new_unique = [x for x in self.uniquebodyparts if x not in self._old_unique] - - # Checking if user added a new label - if not any([self.new_multi, self.new_unique]): # i.e. no new labels - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=self.view_locked, - ) - else: - # Found new labels in either multiple bodyparts or unique bodyparts - dlg = wx.MessageDialog( - None, - "New label found in the config file. Do you want to see all the other labels?", - "New label found", - wx.YES_NO | wx.ICON_WARNING, - ) - result = dlg.ShowModal() - if result == wx.ID_NO: - if self.new_multi: - self.multibodyparts = self.new_multi - if self.new_unique: - self.uniquebodyparts = self.new_unique - - self.dataFrame = MainFrame.create_dataframe( - self, - self.dataFrame, - self.relativeimagenames, - self.individual_names, - self.new_unique, - self.new_multi, - ) - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=self.view_locked, - ) - - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - - if self.individual_names[0] == "single": - ( - self.choiceBox, - self.individualrdb, - self.rdb, - self.change_marker_size, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.uniquebodyparts, self.individual_names, self.file, self.markerSize - ) - self.image_panel.addcolorbar( - self.img, - self.image_axis, - self.iter, - self.uniquebodyparts, - self.colormap, - ) - else: - ( - self.choiceBox, - self.individualrdb, - self.rdb, - self.change_marker_size, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.multibodyparts, self.individual_names, self.file, self.markerSize - ) - self.image_panel.addcolorbar( - self.img, self.image_axis, self.iter, self.multibodyparts, self.colormap - ) - self.individualrdb.Bind(wx.EVT_RADIOBOX, self.select_individual) - # check if single is slected when radio buttons are changed - if self.individualrdb.GetStringSelection() == "single": - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.uniquebodyparts - ) - else: - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.multibodyparts - ) - self.buttonCounter = MainFrame.plot(self, self.img) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activateSlider) - self.change_marker_size.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - - def create_dataframe( - self, - dataFrame, - relativeimagenames, - individual_names, - uniquebodyparts, - multibodyparts, - ): - a = np.empty((len(relativeimagenames), 2)) - a[:] = np.nan - for prfxindex, prefix in enumerate(individual_names): - if uniquebodyparts is not None: - if prefix == "single": - for c, bp in enumerate(uniquebodyparts): - index = pd.MultiIndex.from_product( - [[self.scorer], [prefix], [bp], ["x", "y"]], - names=["scorer", "individuals", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=relativeimagenames) - dataFrame = pd.concat([dataFrame, frame], axis=1) - else: - for c, bp in enumerate(multibodyparts): - index = pd.MultiIndex.from_product( - [[self.scorer], [prefix], [bp], ["x", "y"]], - names=["scorer", "individuals", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=relativeimagenames) - dataFrame = pd.concat([dataFrame, frame], axis=1) - else: - for c, bp in enumerate(multibodyparts): - index = pd.MultiIndex.from_product( - [[self.scorer], [prefix], [bp], ["x", "y"]], - names=["scorer", "individuals", "bodyparts", "coords"], - ) - frame = pd.DataFrame(a, columns=index, index=relativeimagenames) - dataFrame = pd.concat([dataFrame, frame], axis=1) - dataFrame.sort_index(inplace=True) - return dataFrame - - def select_individual(self, event): - individualName = self.individualrdb.GetStringSelection() - self.change_marker_size.Hide() - self.change_marker_size.Destroy() - if individualName == "single": - self.checkBox.Hide() - self.individualrdb.Hide() - self.rdb.Hide() - ( - self.choiceBox, - self.individualrdb, - self.rdb, - self.change_marker_size, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.uniquebodyparts, self.individual_names, self.file, self.markerSize - ) - self.individualrdb.SetStringSelection(individualName) - self.individualrdb.Bind(wx.EVT_RADIOBOX, self.select_individual) - self.figure.delaxes(self.figure.axes[1]) - self.image_panel.addcolorbar( - self.img, - self.image_axis, - self.iter, - self.uniquebodyparts, - self.colormap, - ) - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activateSlider) - self.change_marker_size.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - else: - self.checkBox.Hide() - self.individualrdb.Hide() - self.rdb.Hide() - ( - self.choiceBox, - self.individualrdb, - self.rdb, - self.change_marker_size, - self.checkBox, - ) = self.choice_panel.addRadioButtons( - self.multibodyparts, self.individual_names, self.file, self.markerSize - ) - self.individualrdb.SetStringSelection(individualName) - self.change_marker_size.Show() - self.checkBox.Show() - self.individualrdb.Show() - self.rdb.Show() - self.individualrdb.Bind(wx.EVT_RADIOBOX, self.select_individual) - self.figure.delaxes(self.figure.axes[1]) - self.image_panel.addcolorbar( - self.img, self.image_axis, self.iter, self.multibodyparts, self.colormap - ) - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activateSlider) - self.change_marker_size.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - - def nextImage(self, event): - """ - Moves to next image - """ - self.individualrdb.SetSelection(0) - MainFrame.select_individual(self, event) - # Checks for the last image and disables the Next button - if len(self.index) - self.iter == 1: - self.next.Enable(False) - return - self.prev.Enable(True) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.rdb.SetSelection(0) - self.individualrdb.SetSelection(0) - self.file = 1 - # Refreshing the button counters - self.buttonCounter = {i: [] for i in self.individual_names} - MainFrame.saveEachImage(self) - self.iter = self.iter + 1 - - if len(self.index) >= self.iter: - self.updatedCoords = [] - self.img = self.index[self.iter] - img_name = Path(self.index[self.iter]).name - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - if self.individualrdb.GetStringSelection() == "single": - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.uniquebodyparts - ) - self.buttonCounter = MainFrame.plot(self, self.img) - else: - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.multibodyparts - ) - self.buttonCounter = MainFrame.plot(self, self.img) - - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - - def prevImage(self, event): - """ - Checks the previous Image and enables user to move the annotations. - """ - self.individualrdb.SetSelection(0) - MainFrame.select_individual(self, event) - MainFrame.saveEachImage(self) - # Checks for the first image and disables the Previous button - if self.iter == 0: - self.prev.Enable(False) - return - else: - self.next.Enable(True) - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.buttonCounter = {i: [] for i in self.individual_names} - self.iter = self.iter - 1 - - self.rdb.SetSelection(0) - self.individualrdb.SetSelection(0) - self.updatedCoords = [] - self.img = self.index[self.iter] - img_name = Path(self.index[self.iter]).name - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.image_axis, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.multibodyparts, - self.colormap, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - self.buttonCounter = MainFrame.plot(self, self.img) - - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - - def plot(self, img): - """ - Plots and call auxfun_drag class for moving and removing points. - """ - self.drs = [] - self.updatedCoords = [] - for j, ind in enumerate(self.individual_names): - idcolor = self.idmap(j) - if ind == "single": - for c, bp in enumerate(self.uniquebodyparts): - image_points = [ - [ - self.dataFrame[self.scorer][ind][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][ind][bp]["y"].values[self.iter], - ind, - bp, - ] - ] - self.points = [ - self.dataFrame[self.scorer][ind][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][ind][bp]["y"].values[self.iter], - ] - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.uniquebodyparts - ) - color = self.colormap(self.norm(self.colorIndex[c])) - circle = patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - ec=idcolor, - lw=self.edgewidth, - alpha=self.alpha, - ) - self.axes.add_patch(circle) - self.dr = auxfun_drag.DraggablePoint( - circle, self.uniquebodyparts[c], individual_names=ind - ) - self.dr.connect() - self.dr.coords = image_points - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - if not np.isnan(self.points)[0]: - self.buttonCounter[ind].append(self.uniquebodyparts[c]) - else: - for c, bp in enumerate(self.multibodyparts): - image_points = [ - [ - self.dataFrame[self.scorer][ind][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][ind][bp]["y"].values[self.iter], - ind, - bp, - ] - ] - self.points = [ - self.dataFrame[self.scorer][ind][bp]["x"].values[self.iter], - self.dataFrame[self.scorer][ind][bp]["y"].values[self.iter], - ] - self.norm, self.colorIndex = self.image_panel.getColorIndices( - self.img, self.multibodyparts - ) - color = self.colormap(self.norm(self.colorIndex[c])) - circle = patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - ec=idcolor, - lw=self.edgewidth, - alpha=self.alpha, - ) - self.axes.add_patch(circle) - self.dr = auxfun_drag.DraggablePoint( - circle, self.multibodyparts[c], individual_names=ind - ) - self.dr.connect() - self.dr.coords = image_points - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - if not np.isnan(self.points)[0]: - self.buttonCounter[ind].append(self.multibodyparts[c]) - MainFrame.saveEachImage(self) - self.figure.canvas.draw() - return self.buttonCounter - - def saveEachImage(self): - """ - Saves data for each image - """ - - for idx, bp in enumerate(self.updatedCoords): - self.dataFrame.loc[self.relativeimagenames[self.iter]][ - self.scorer, bp[-1][2], bp[0][-1], "x" - ] = bp[-1][0] - self.dataFrame.loc[self.relativeimagenames[self.iter]][ - self.scorer, bp[-1][2], bp[0][-1], "y" - ] = bp[-1][1] - - def saveDataSet(self, event): - """ - Saves the final dataframe - """ - self.statusbar.SetStatusText("File saved") - MainFrame.saveEachImage(self) - MainFrame.updateZoomPan(self) - - # Windows compatible - self.dataFrame.sort_index(inplace=True) - # Discard data associated with bodyparts that are no longer in the config - config_bpts = self.cfg["multianimalbodyparts"] + self.cfg["uniquebodyparts"] - valid = [ - bp in config_bpts - for bp in self.dataFrame.columns.get_level_values("bodyparts") - ] - self.dataFrame = self.dataFrame.loc[:, valid] - # Re-organize the dataframe so the CSV looks consistent with the config - self.dataFrame = self.dataFrame.reindex( - columns=self.individual_names, level="individuals" - ).reindex(columns=config_bpts, level="bodyparts") - self.dataFrame.to_csv( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".csv") - ) - self.dataFrame.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.scorer + ".h5"), - "df_with_missing", - format="table", - mode="w", - ) - - def onChecked(self, event): - self.cb = event.GetEventObject() - if self.cb.GetValue(): - self.change_marker_size.Enable(True) - self.cidClick = self.canvas.mpl_connect("button_press_event", self.onClick) - else: - self.change_marker_size.Enable(False) - - -def show(config, config3d, sourceCam): - app = wx.App() - frame = MainFrame(None, config, config3d, sourceCam).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config") - parser.add_argument("config3d") - parser.add_argument("sourceCam") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/multiple_individuals_refinement_toolbox.py b/deeplabcut/gui/multiple_individuals_refinement_toolbox.py deleted file mode 100644 index 225385672b..0000000000 --- a/deeplabcut/gui/multiple_individuals_refinement_toolbox.py +++ /dev/null @@ -1,1167 +0,0 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/DeepLabCutDeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" - -import argparse -import os -import os.path -import platform -from pathlib import Path - -# from skimage import io -import PIL -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import wx -import wx.lib.scrolledpanel as SP -from matplotlib.backends.backend_wxagg import ( - NavigationToolbar2WxAgg as NavigationToolbar, -) -from mpl_toolkits.axes_grid1 import make_axes_locatable -from skimage import io - -from deeplabcut.gui import auxfun_drag -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import auxiliaryfunctions, visualization - - -# ########################################################################### -# Class for GUI MainFrame -# ########################################################################### -class ImagePanel(BasePanel): - def drawplot( - self, img, img_name, itr, index, threshold, cmap, preview, keep_view=False - ): - xlim = self.axes.get_xlim() - ylim = self.axes.get_ylim() - self.axes.clear() - im = io.imread(img) - self.ax = self.axes.imshow(im, cmap=cmap) - self.orig_xlim = self.axes.get_xlim() - self.orig_ylim = self.axes.get_ylim() - if not preview: - self.axes.set_title( - str( - str(itr) - + "/" - + str(len(index) - 1) - + " " - + str(Path(index[itr]).stem) - + " " - + " Threshold chosen is: " - + str("{0:.2f}".format(threshold)) - ) - ) - else: - self.axes.set_title( - str( - str(itr) - + "/" - + str(len(index) - 1) - + " " - + str(Path(index[itr]).stem) - ) - ) - if keep_view: - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) - self.figure.canvas.draw() - if not hasattr(self, "toolbar"): - self.toolbar = NavigationToolbar(self.canvas) - return (self.figure, self.axes, self.canvas, self.toolbar, self.ax) - - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - im = io.imread(img) - norm = mcolors.Normalize(vmin=np.min(im), vmax=np.max(im)) - ticks = np.linspace(np.min(im), np.max(im), len(bodyparts))[::-1] - return norm, ticks - - -class ScrollPanel(SP.ScrolledPanel): - def __init__(self, parent): - SP.ScrolledPanel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) - self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - self.Layout() - - def on_focus(self, event): - pass - - def addCheckBoxSlider(self, bodyparts, fileIndex, markersize): - """ - Adds checkbox and a slider - """ - self.choiceBox = wx.BoxSizer(wx.VERTICAL) - - self.slider = wx.Slider( - self, - -1, - markersize, - 1, - markersize * 3, - size=(250, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - self.slider.Enable(False) - self.checkBox = wx.CheckBox(self, id=wx.ID_ANY, label="Adjust marker size.") - self.choiceBox.Add(self.slider, 0, wx.ALL, 5) - self.choiceBox.Add(self.checkBox, 0, wx.ALL, 5) - names = ["Color individuals", "Color bodyparts"] - self.visualization_radiobox = wx.RadioBox( - self, - label="Select the visualization scheme", - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - choices=names, - ) - self.choiceBox.Add(self.visualization_radiobox, 0, wx.EXPAND | wx.ALL, 10) - - self.SetSizerAndFit(self.choiceBox) - self.Layout() - return (self.choiceBox, self.slider, self.checkBox, self.visualization_radiobox) - - def clearBoxer(self): - self.choiceBox.Clear(True) - - -class MainFrame(BaseFrame): - def __init__(self, parent, config): - super(MainFrame, self).__init__("DeepLabCut - Refinement ToolBox", parent) - self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) - - ################################################################################################################################################### - - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - - topSplitter = wx.SplitterWindow(self) - vSplitter = wx.SplitterWindow(topSplitter) - - self.image_panel = ImagePanel(vSplitter, config, self.gui_size) - self.choice_panel = ScrollPanel(vSplitter) - # self.choice_panel.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - # self.choice_panel.SetupScrolling(scroll_x=True, scrollToTop=False) - vSplitter.SplitVertically( - self.image_panel, self.choice_panel, sashPosition=self.gui_size[0] * 0.8 - ) - vSplitter.SetSashGravity(1) - self.widget_panel = WidgetPanel(topSplitter) - topSplitter.SplitHorizontally( - vSplitter, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - self.load = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Load labels") - widgetsizer.Add(self.load, 1, wx.ALL, 15) - self.load.Bind(wx.EVT_BUTTON, self.browseDir) - - self.prev = wx.Button(self.widget_panel, id=wx.ID_ANY, label="< self.iter: - self.updatedCoords = [] - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - # Plotting - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - if self.visualization_rdb.GetSelection() == 0: - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.ax, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - else: - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.ax, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - im = io.imread(self.img) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - if np.max(im) == 0: - msg = wx.MessageBox( - "Invalid image. Click Yes to remove", - "Error!", - wx.YES_NO | wx.ICON_WARNING, - ) - if msg == 2: - self.Dataframe = self.Dataframe.drop(self.index[self.iter]) - self.index = list(self.Dataframe.iloc[:, 0].index) - self.iter = self.iter - 1 - - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.ax, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - MainFrame.plot(self, self.img) - else: - self.next.Enable(False) - MainFrame.saveEachImage(self) - - def prevImage(self, event): - """ - Checks the previous Image and enables user to move the annotations. - """ - - MainFrame.saveEachImage(self) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.next.Enable(True) - self.iter = self.iter - 1 - - # Checks for the first image and disables the Previous button - if self.iter == 0: - self.prev.Enable(False) - - if self.iter >= 0: - self.updatedCoords = [] - # Reading Image - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - # Plotting - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - if self.visualization_rdb.GetSelection() == 0: - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.ax, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - else: - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - self.ax, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - MainFrame.plot(self, self.img) - else: - self.prev.Enable(False) - MainFrame.saveEachImage(self) - - def quitButton(self, event): - """ - Quits the GUI - """ - self.statusbar.SetStatusText("") - dlg = wx.MessageDialog( - None, "Are you sure?", "Quit!", wx.YES_NO | wx.ICON_WARNING - ) - result = dlg.ShowModal() - if result == wx.ID_YES: - print( - "Closing... The refined labels are stored in a subdirectory under labeled-data. Use the function 'merge_datasets' to augment the training dataset, and then re-train a network using create_training_dataset followed by train_network!" - ) - self.Destroy() - else: - self.save.Enable(True) - - def helpButton(self, event): - """ - Opens Instructions - """ - self.statusbar.SetStatusText("Help") - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - wx.MessageBox( - "1. Enter the likelihood threshold. \n\n2. All the data points above the threshold will be marked as circle filled with a unique color. All the data points below the threshold will be marked with a hollow circle. \n\n3. Enable the checkbox to adjust the marker size (you will not be able to zoom/pan/home until the next frame). \n\n4. Hover your mouse over data points to see the labels and their likelihood. \n\n5. LEFT click+drag to move the data points. \n\n6. RIGHT click on any data point to remove it. Be careful, you cannot undo this step! \n Click once on the zoom button to zoom-in the image. The cursor will become cross, click and drag over a point to zoom in. \n Click on the zoom button again to disable the zooming function and recover the cursor. \n Use pan button to pan across the image while zoomed in. Use home button to go back to the full default view. \n\n7. When finished click 'Save' to save all the changes. \n\n8. Click OK to continue", - "User instructions", - wx.OK | wx.ICON_INFORMATION, - ) - - def onChecked(self, event): - MainFrame.saveEachImage(self) - self.cb = event.GetEventObject() - if self.cb.GetValue(): - self.slider.Enable(True) - else: - self.slider.Enable(False) - - def force_outside_labels_Nans(self, index, ind, bodyparts): - """ - Checks the dataframe for any labels outside the image and forces them Nans. - """ - for bpindex, bp in enumerate(bodyparts): - testCondition = ( - self.Dataframe.loc[index, (self.scorer, ind, bp, "x")] > self.width - or self.Dataframe.loc[index, (self.scorer, ind, bp, "x")] < 0 - or self.Dataframe.loc[index, (self.scorer, ind, bp, "y")] > self.height - or self.Dataframe.loc[index, (self.scorer, ind, bp, "y")] < 0 - ) - if testCondition: - print("Found %s outside the image %s.Setting it to NaN" % (bp, index)) - self.Dataframe.loc[index, (self.scorer, ind, bp, "x")] = np.nan - self.Dataframe.loc[index, (self.scorer, ind, bp, "y")] = np.nan - return self.Dataframe - - def check_labels(self): - print("Checking labels if they are outside the image") - for i in self.Dataframe.index: - image_name = os.path.join(self.project_path, i) - im = PIL.Image.open(image_name) - self.width, self.height = im.size - for ind in self.individual_names: - if ind == "single": - self.Dataframe = MainFrame.force_outside_labels_Nans( - self, i, ind, self.uniquebodyparts - ) - else: - self.Dataframe = MainFrame.force_outside_labels_Nans( - self, i, ind, self.multianimalbodyparts - ) - return self.Dataframe - - def saveDataSet(self, event): - - MainFrame.saveEachImage(self) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - self.statusbar.SetStatusText("File saved") - - self.Dataframe = MainFrame.check_labels(self) - # Overwrite machine label file - self.Dataframe.to_hdf(self.dataname, key="df_with_missing", mode="w") - - self.Dataframe.columns.set_levels( - [self.scorer.replace(self.scorer, self.humanscorer)], level=0, inplace=True - ) - self.Dataframe = self.Dataframe.drop("likelihood", axis=1, level=3) - - if Path(self.dir, "CollectedData_" + self.humanscorer + ".h5").is_file(): - print( - "A training dataset file is already found for this video. The refined machine labels are merged to this data!" - ) - DataU1 = pd.read_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5") - ) - # combine datasets Original Col. + corrected machinefiles: - DataCombined = pd.concat([self.Dataframe, DataU1]) - # Now drop redundant ones keeping the first one [this will make sure that the refined machine file gets preference] - DataCombined = DataCombined[~DataCombined.index.duplicated(keep="first")] - """ - if len(self.droppedframes)>0: #i.e. frames were dropped/corrupt. also remove them from original file (if they exist!) - for fn in self.droppedframes: - try: - DataCombined.drop(fn,inplace=True) - except KeyError: - pass - """ - DataCombined.sort_index(inplace=True) - DataCombined.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5"), - key="df_with_missing", - mode="w", - ) - DataCombined.to_csv( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".csv") - ) - else: - self.Dataframe.sort_index(inplace=True) - self.Dataframe.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5"), - key="df_with_missing", - mode="w", - ) - self.Dataframe.to_csv( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".csv") - ) - self.next.Enable(False) - self.prev.Enable(False) - self.slider.Enable(False) - self.checkBox.Enable(False) - - nextFilemsg = wx.MessageBox( - "File saved. Do you want to refine another file?", - "Repeat?", - wx.YES_NO | wx.ICON_INFORMATION, - ) - if nextFilemsg == 2: - self.file = 1 - self.axes.clear() - self.figure.delaxes(self.figure.axes[1]) - self.choiceBox.Clear(True) - MainFrame.updateZoomPan(self) - self.load.Enable(True) - MainFrame.browseDir(self, event) - - # ########################################################################### - # Other functions - # ########################################################################### - def saveEachImage(self): - """ - Updates the dataframe for the current image with the new datapoints - """ - - for bpindex, bp in enumerate(self.updatedCoords): - self.Dataframe.loc[self.Dataframe.index[self.iter]][ - self.scorer, bp[0][-1], bp[0][-3], "x" - ] = self.updatedCoords[bpindex][-1][0] - self.Dataframe.loc[self.Dataframe.index[self.iter]][ - self.scorer, bp[0][-1], bp[0][-3], "y" - ] = self.updatedCoords[bpindex][-1][1] - - def getLabels(self, img_index, ind, bodyparts): - """ - Returns a list of x and y labels of the corresponding image index - """ - self.previous_image_points = [] - for bpindex, bp in enumerate(bodyparts): - image_points = [ - [ - self.Dataframe[self.scorer][ind][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["y"].values[self.iter], - bp, - bpindex, - ind, - ] - ] - self.previous_image_points.append(image_points) - return self.previous_image_points - - def plot(self, im): - """ - Plots and call auxfun_drag class for moving and removing points. - """ - # small hack in case there are any 0 intensity images! - img = io.imread(im) - maxIntensity = np.max(img) - if maxIntensity == 0: - maxIntensity = np.max(img) + 255 - - divider = make_axes_locatable(self.axes) - cax = divider.append_axes("right", size="5%", pad=0.05) - self.drs = [] - - if ( - self.visualization_rdb.GetSelection() == 0 - ): # i.e. for color scheme for individuals - self.Colorscheme = visualization.get_cmap( - len(self.individual_names), self.cfg["colormap"] - ) - self.norm, self.colorIndex = self.image_panel.getColorIndices( - im, self.individual_names - ) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.individual_names) - else: # i.e. for color scheme for all bodyparts - self.Colorscheme = visualization.get_cmap( - len(self.all_bodyparts), self.cfg["colormap"] - ) - self.norm, self.colorIndex = self.image_panel.getColorIndices( - im, self.all_bodyparts - ) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.all_bodyparts) - - for ci, ind in enumerate(self.individual_names): - col_idx = ( - 0 # variable for iterating through the colorscheme for all bodyparts - ) - image_points = [] - if ind == "single": - if self.visualization_rdb.GetSelection() == 0: - for c, bp in enumerate(self.uniquebodyparts): - self.points = [ - self.Dataframe[self.scorer][ind][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["y"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["likelihood"].values[ - self.iter - ], - ] - self.likelihood = self.points[2] - - # fix move to corner - if self.move2corner: - ny, nx = np.shape(img)[0], np.shape(img)[1] - if self.points[0] > nx or self.points[0] < 0: - print("fixing x for ", bp) - self.points[0] = self.center[0] - if self.points[1] > ny or self.points[1] < 0: - print("fixing y for ", bp) - self.points[1] = self.center[1] - - if self.likelihood < self.threshold: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - facecolor="None", - edgecolor=self.Colorscheme(ci), - alpha=self.alpha, - ) - ] - else: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=self.Colorscheme(ci), - alpha=self.alpha, - ) - ] - self.axes.add_patch(self.circle[0]) - self.dr = auxfun_drag.DraggablePoint( - self.circle[0], - bp, - individual_names=ind, - likelihood=self.likelihood, - ) - self.dr.connect() - self.dr.coords = MainFrame.getLabels( - self, self.iter, ind, self.uniquebodyparts - )[c] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - else: - for c, bp in enumerate(self.uniquebodyparts): - self.points = [ - self.Dataframe[self.scorer][ind][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["y"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["likelihood"].values[ - self.iter - ], - ] - self.likelihood = self.points[2] - - # fix move to corner - if self.move2corner: - ny, nx = np.shape(img)[0], np.shape(img)[1] - if self.points[0] > nx or self.points[0] < 0: - print("fixing x for ", bp) - self.points[0] = self.center[0] - if self.points[1] > ny or self.points[1] < 0: - print("fixing y for ", bp) - self.points[1] = self.center[1] - - if self.likelihood < self.threshold: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc="None", - edgecolor=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - ] - else: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - ] - self.axes.add_patch(self.circle[0]) - col_idx = col_idx + 1 - self.dr = auxfun_drag.DraggablePoint( - self.circle[0], - bp, - individual_names=ind, - likelihood=self.likelihood, - ) - self.dr.connect() - self.dr.coords = MainFrame.getLabels( - self, self.iter, ind, self.uniquebodyparts - )[c] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - else: - if self.visualization_rdb.GetSelection() == 0: - for c, bp in enumerate(self.multianimalbodyparts): - self.points = [ - self.Dataframe[self.scorer][ind][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["y"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["likelihood"].values[ - self.iter - ], - ] - self.likelihood = self.points[2] - - # fix move to corner - if self.move2corner: - ny, nx = np.shape(img)[0], np.shape(img)[1] - if self.points[0] > nx or self.points[0] < 0: - print("fixing x for ", bp) - self.points[0] = self.center[0] - if self.points[1] > ny or self.points[1] < 0: - print("fixing y for ", bp) - self.points[1] = self.center[1] - - if self.likelihood < self.threshold: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc="None", - edgecolor=self.Colorscheme(ci), - alpha=self.alpha, - ) - ] - else: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=self.Colorscheme(ci), - alpha=self.alpha, - ) - ] - self.axes.add_patch(self.circle[0]) - self.dr = auxfun_drag.DraggablePoint( - self.circle[0], - bp, - individual_names=ind, - likelihood=self.likelihood, - ) - self.dr.connect() - self.dr.coords = MainFrame.getLabels( - self, self.iter, ind, self.multianimalbodyparts - )[c] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - else: - for c, bp in enumerate(self.multianimalbodyparts): - self.points = [ - self.Dataframe[self.scorer][ind][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["y"].values[self.iter], - self.Dataframe[self.scorer][ind][bp]["likelihood"].values[ - self.iter - ], - ] - self.likelihood = self.points[2] - - # fix move to corner - if self.move2corner: - ny, nx = np.shape(img)[0], np.shape(img)[1] - if self.points[0] > nx or self.points[0] < 0: - print("fixing x for ", bp) - self.points[0] = self.center[0] - if self.points[1] > ny or self.points[1] < 0: - print("fixing y for ", bp) - self.points[1] = self.center[1] - - if self.likelihood < self.threshold: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc="None", - edgecolor=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - ] - else: - self.circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - ] - self.axes.add_patch(self.circle[0]) - col_idx = col_idx + 1 - self.dr = auxfun_drag.DraggablePoint( - self.circle[0], - bp, - individual_names=ind, - likelihood=self.likelihood, - ) - self.dr.connect() - self.dr.coords = MainFrame.getLabels( - self, self.iter, ind, self.multianimalbodyparts - )[c] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - self.figure.canvas.draw() - - -def show(config): - app = wx.App() - frame = MainFrame(None, config).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/outlier_frame_extraction_toolbox.py b/deeplabcut/gui/outlier_frame_extraction_toolbox.py deleted file mode 100644 index b9260f772d..0000000000 --- a/deeplabcut/gui/outlier_frame_extraction_toolbox.py +++ /dev/null @@ -1,592 +0,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 argparse -import os -from pathlib import Path - -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import wx -import wx.lib.scrolledpanel as SP -from matplotlib.figure import Figure -from mpl_toolkits.axes_grid1 import make_axes_locatable -from skimage import io -from skimage.util import img_as_ubyte - -from deeplabcut.create_project import add -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import auxiliaryfunctions, visualization -from deeplabcut.utils.auxfun_videos import VideoWriter - - -# ########################################################################### -# Class for GUI MainFrame -# ########################################################################### -class ImagePanel(BasePanel): - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - # im = io.imread(img) - norm = mcolors.Normalize(vmin=np.min(img), vmax=np.max(img)) - ticks = np.linspace(np.min(img), np.max(img), len(bodyparts))[::-1] - return norm, ticks - - -class ScrollPanel(SP.ScrolledPanel): - def __init__(self, parent): - SP.ScrolledPanel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) - self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - self.Layout() - - def on_focus(self, event): - pass - - def addRadioButtons(self): - """ - Adds radio buttons for each bodypart on the right panel - """ - self.choiceBox = wx.BoxSizer(wx.VERTICAL) - names = ["Color individuals", "Color bodyparts"] - self.visualization_radiobox = wx.RadioBox( - self, - label="Select the visualization scheme", - majorDimension=1, - style=wx.RA_SPECIFY_COLS, - choices=names, - ) - self.choiceBox.Add(self.visualization_radiobox, 0, wx.EXPAND | wx.ALL, 10) - - self.SetSizerAndFit(self.choiceBox) - self.Layout() - return (self.choiceBox, self.visualization_radiobox) - - -class MainFrame(BaseFrame): - """Contains the main GUI and button boxes""" - - def __init__( - self, parent, config, video, shuffle, Dataframe, savelabeled, multianimal - ): - super(MainFrame, self).__init__( - "DeepLabCut2.0 - Manual Outlier Frame Extraction", parent - ) - - ################################################################################################################################################### - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - # topSplitter = wx.SplitterWindow(self) - # - # self.image_panel = ImagePanel(topSplitter, config,video,shuffle,Dataframe,self.gui_size) - # self.widget_panel = WidgetPanel(topSplitter) - # - # topSplitter.SplitHorizontally(self.image_panel, self.widget_panel,sashPosition=self.gui_size[1]*0.83)#0.9 - # topSplitter.SetSashGravity(1) - # sizer = wx.BoxSizer(wx.VERTICAL) - # sizer.Add(topSplitter, 1, wx.EXPAND) - # self.SetSizer(sizer) - - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - - topSplitter = wx.SplitterWindow(self) - vSplitter = wx.SplitterWindow(topSplitter) - - self.image_panel = ImagePanel(vSplitter, config, self.gui_size) - self.choice_panel = ScrollPanel(vSplitter) - - vSplitter.SplitVertically( - self.image_panel, self.choice_panel, sashPosition=self.gui_size[0] * 0.8 - ) - vSplitter.SetSashGravity(1) - self.widget_panel = WidgetPanel(topSplitter) - topSplitter.SplitHorizontally( - vSplitter, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - - self.load_button_sizer = wx.BoxSizer(wx.VERTICAL) - self.help_button_sizer = wx.BoxSizer(wx.VERTICAL) - - self.help = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Help") - self.help_button_sizer.Add(self.help, 1, wx.ALL, 15) - # widgetsizer.Add(self.help , 1, wx.ALL, 15) - self.help.Bind(wx.EVT_BUTTON, self.helpButton) - - widgetsizer.Add(self.help_button_sizer, 1, wx.ALL, 0) - - self.grab = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Grab Frames") - widgetsizer.Add(self.grab, 1, wx.ALL, 15) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.grab.Enable(True) - - widgetsizer.AddStretchSpacer(5) - self.slider = wx.Slider( - self.widget_panel, - id=wx.ID_ANY, - value=0, - minValue=0, - maxValue=1, - size=(200, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - widgetsizer.Add(self.slider, 1, wx.ALL, 5) - self.slider.Bind(wx.EVT_SLIDER, self.OnSliderScroll) - - widgetsizer.AddStretchSpacer(5) - self.start_frames_sizer = wx.BoxSizer(wx.VERTICAL) - self.end_frames_sizer = wx.BoxSizer(wx.VERTICAL) - - self.start_frames_sizer.AddSpacer(15) - # self.startFrame = wx.SpinCtrl(self.widget_panel, value='0', size=(100, -1), min=0, max=120) - self.startFrame = wx.SpinCtrl( - self.widget_panel, value="0", size=(100, -1) - ) # ,style=wx.SP_VERTICAL) - self.startFrame.Enable(False) - self.start_frames_sizer.Add(self.startFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - start_text = wx.StaticText(self.widget_panel, label="Start Frame Index") - self.start_frames_sizer.Add(start_text, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.checkBox = wx.CheckBox( - self.widget_panel, id=wx.ID_ANY, label="Range of frames" - ) - self.checkBox.Bind(wx.EVT_CHECKBOX, self.activate_frame_range) - self.start_frames_sizer.Add(self.checkBox, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - # - self.end_frames_sizer.AddSpacer(15) - self.endFrame = wx.SpinCtrl( - self.widget_panel, value="1", size=(160, -1) - ) # , min=1, max=120) - self.endFrame.Enable(False) - self.end_frames_sizer.Add(self.endFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - end_text = wx.StaticText(self.widget_panel, label="Number of Frames") - self.end_frames_sizer.Add(end_text, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.updateFrame = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Update") - self.end_frames_sizer.Add(self.updateFrame, 1, wx.EXPAND | wx.ALIGN_LEFT, 15) - self.updateFrame.Bind(wx.EVT_BUTTON, self.updateSlider) - self.updateFrame.Enable(False) - - widgetsizer.Add(self.start_frames_sizer, 1, wx.ALL, 0) - widgetsizer.AddStretchSpacer(5) - widgetsizer.Add(self.end_frames_sizer, 1, wx.ALL, 0) - widgetsizer.AddStretchSpacer(15) - - self.quit = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Quit") - widgetsizer.Add(self.quit, 1, wx.ALL, 15) - self.quit.Bind(wx.EVT_BUTTON, self.quitButton) - self.quit.Enable(True) - - self.widget_panel.SetSizer(widgetsizer) - self.widget_panel.SetSizerAndFit(widgetsizer) - - # Variables initialization - self.numberFrames = 0 - self.currFrame = 0 - self.figure = Figure() - self.axes = self.figure.add_subplot(111) - self.drs = [] - self.extract_range_frame = False - self.firstFrame = 0 - self.Colorscheme = [] - - # Read confing file - self.cfg = auxiliaryfunctions.read_config(config) - self.Task = self.cfg["Task"] - self.start = self.cfg["start"] - self.stop = self.cfg["stop"] - self.date = self.cfg["date"] - self.trainFraction = self.cfg["TrainingFraction"] - self.trainFraction = self.trainFraction[0] - self.videos = self.cfg["video_sets"].keys() - self.bodyparts = self.cfg["bodyparts"] - self.colormap = plt.get_cmap(self.cfg["colormap"]) - self.colormap = self.colormap.reversed() - self.markerSize = self.cfg["dotsize"] - self.alpha = self.cfg["alphavalue"] - self.iterationindex = self.cfg["iteration"] - self.cropping = self.cfg["cropping"] - self.video_names = [Path(i).stem for i in self.videos] - self.config_path = Path(config) - self.video_source = Path(video).resolve() - self.shuffle = shuffle - self.Dataframe = Dataframe - self.savelabeled = savelabeled - self.multianimal = multianimal - if self.multianimal: - from deeplabcut.utils import auxfun_multianimal - - ( - self.individual_names, - self.uniquebodyparts, - self.multianimalbodyparts, - ) = auxfun_multianimal.extractindividualsandbodyparts(self.cfg) - self.choiceBox, self.visualization_rdb = self.choice_panel.addRadioButtons() - self.Colorscheme = visualization.get_cmap( - len(self.individual_names), self.cfg["colormap"] - ) - self.visualization_rdb.Bind(wx.EVT_RADIOBOX, self.clear_plot) - # Read the video file - self.vid = VideoWriter(str(self.video_source)) - if self.cropping: - self.vid.set_bbox( - self.cfg["x1"], self.cfg["x2"], self.cfg["y1"], self.cfg["y2"] - ) - self.filename = Path(self.video_source).name - self.numberFrames = len(self.vid) - self.strwidth = int(np.ceil(np.log10(self.numberFrames))) - # Set the values of slider and range of frames - self.startFrame.SetMax(self.numberFrames - 1) - self.slider.SetMax(self.numberFrames - 1) - self.endFrame.SetMax(self.numberFrames - 1) - self.startFrame.Bind(wx.EVT_SPINCTRL, self.updateSlider) # wx.EVT_SPIN - # Set the status bar - self.statusbar.SetStatusText("Working on video: {}".format(self.filename)) - # Adding the video file to the config file. - if self.vid.name not in self.video_names: - add.add_new_videos(self.config_path, [self.video_source]) - - self.update() - self.plot_labels() - self.widget_panel.Layout() - - def quitButton(self, event): - """ - Quits the GUI - """ - self.statusbar.SetStatusText("") - dlg = wx.MessageDialog( - None, "Are you sure?", "Quit!", wx.YES_NO | wx.ICON_WARNING - ) - result = dlg.ShowModal() - if result == wx.ID_YES: - print("Quitting for now!") - self.Destroy() - - def updateSlider(self, event): - self.slider.SetValue(self.startFrame.GetValue()) - self.startFrame.SetValue(self.slider.GetValue()) - self.axes.clear() - self.figure.delaxes(self.figure.axes[1]) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.currFrame = self.slider.GetValue() - self.update() - self.plot_labels() - - def activate_frame_range(self, event): - """ - Activates the frame range boxes - """ - self.checkSlider = event.GetEventObject() - if self.checkSlider.GetValue(): - self.extract_range_frame = True - self.startFrame.Enable(True) - self.startFrame.SetValue(self.slider.GetValue()) - self.endFrame.Enable(True) - self.updateFrame.Enable(True) - self.grab.Enable(False) - else: - self.extract_range_frame = False - self.startFrame.Enable(False) - self.endFrame.Enable(False) - self.updateFrame.Enable(False) - self.grab.Enable(True) - - def line_select_callback(self, eclick, erelease): - "eclick and erelease are the press and release events" - self.new_x1, self.new_y1 = eclick.xdata, eclick.ydata - self.new_x2, self.new_y2 = erelease.xdata, erelease.ydata - - def OnSliderScroll(self, event): - """ - Slider to scroll through the video - """ - self.axes.clear() - self.figure.delaxes(self.figure.axes[1]) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.currFrame = self.slider.GetValue() - self.startFrame.SetValue(self.currFrame) - self.update() - self.plot_labels() - - def update(self): - """ - Updates the image with the current slider index - """ - self.grab.Enable(True) - self.grab.Bind(wx.EVT_BUTTON, self.grabFrame) - self.figure, self.axes, self.canvas = self.image_panel.getfigure() - self.vid.set_to_frame(self.currFrame) - frame = self.vid.read_frame(crop=self.cropping) - if frame is not None: - frame = img_as_ubyte(frame) - self.ax = self.axes.imshow(frame, cmap=self.colormap) - self.axes.set_title( - str( - str(self.currFrame) - + "/" - + str(self.numberFrames - 1) - + " " - + self.filename - ) - ) - self.figure.canvas.draw() - else: - print("Invalid frame") - - def chooseFrame(self): - frame = img_as_ubyte(self.vid.read_frame(crop=self.cropping)) - fname = Path(self.filename) - output_path = self.config_path.parents[0] / "labeled-data" / fname.stem - - self.machinefile = os.path.join( - str(output_path), "machinelabels-iter" + str(self.iterationindex) + ".h5" - ) - name = str(fname.stem) - DF = self.Dataframe.iloc[[self.currFrame]] - DF.index = [ - os.path.join( - "labeled-data", name, "img" + str(index).zfill(self.strwidth) + ".png" - ) - for index in DF.index - ] - img_name = ( - str(output_path) - + "/img" - + str(self.currFrame).zfill(int(np.ceil(np.log10(self.numberFrames)))) - + ".png" - ) - labeled_img_name = ( - str(output_path) - + "/img" - + str(self.currFrame).zfill(int(np.ceil(np.log10(self.numberFrames)))) - + "labeled.png" - ) - - # Check for it output path and a machine label file exist - if output_path.exists() and Path(self.machinefile).is_file(): - io.imsave(img_name, frame) - if self.savelabeled: - self.figure.savefig(labeled_img_name, bbox_inches="tight") - Data = pd.read_hdf(self.machinefile) - DataCombined = pd.concat([Data, DF]) - DataCombined = DataCombined[~DataCombined.index.duplicated(keep="first")] - DataCombined.to_hdf(self.machinefile, key="df_with_missing", mode="w") - DataCombined.to_csv(os.path.join(str(output_path), "machinelabels.csv")) - # If machine label file does not exist then create one - elif output_path.exists() and not (Path(self.machinefile).is_file()): - if self.savelabeled: - self.figure.savefig(labeled_img_name, bbox_inches="tight") - io.imsave(img_name, frame) - # cv2.imwrite(img_name, frame) - DF.to_hdf(self.machinefile, key="df_with_missing", mode="w") - DF.to_csv(os.path.join(str(output_path), "machinelabels.csv")) - else: - print( - "%s path not found. Please make sure that the video was added to the config file using the function 'deeplabcut.add_new_videos'.Quitting for now!" - % output_path - ) - self.Destroy() - - def grabFrame(self, event): - """ - Extracts the frame and saves in the current directory - """ - - if self.extract_range_frame: - num_frames_extract = self.endFrame.GetValue() - for i in range(self.currFrame, self.currFrame + num_frames_extract): - self.currFrame = i - self.vid.set_to_frame(self.currFrame) - self.chooseFrame() - else: - self.vid.set_to_frame(self.currFrame) - self.chooseFrame() - - def clear_plot(self, event): - self.figure.delaxes(self.figure.axes[1]) - [p.remove() for p in reversed(self.axes.patches)] - self.plot_labels() - - def plot_labels(self): - """ - Plots the labels of the analyzed video - """ - self.vid.set_to_frame(self.currFrame) - frame = self.vid.read_frame() - if frame is not None: - divider = make_axes_locatable(self.axes) - cax = divider.append_axes("right", size="5%", pad=0.05) - if self.multianimal: - # take into account of all the bodyparts for the colorscheme. Sort the bodyparts to have same order as in the config file - self.all_bodyparts = np.array( - self.multianimalbodyparts + self.uniquebodyparts - ) - _, return_idx = np.unique(self.all_bodyparts, return_index=True) - self.all_bodyparts = list(self.all_bodyparts[np.sort(return_idx)]) - - if ( - self.visualization_rdb.GetSelection() == 0 - ): # i.e. for color scheme for individuals - self.Colorscheme = visualization.get_cmap( - len(self.individual_names), self.cfg["colormap"] - ) - self.norm, self.colorIndex = self.image_panel.getColorIndices( - frame, self.individual_names - ) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.individual_names) - else: # i.e. for color scheme for all bodyparts - self.Colorscheme = visualization.get_cmap( - len(self.all_bodyparts), self.cfg["colormap"] - ) - self.norm, self.colorIndex = self.image_panel.getColorIndices( - frame, self.all_bodyparts - ) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.all_bodyparts) - - for ci, ind in enumerate(self.individual_names): - col_idx = ( - 0 - ) # variable for iterating through the colorscheme for all bodyparts - image_points = [] - if ind == "single": - if self.visualization_rdb.GetSelection() == 0: - for c, bp in enumerate(self.uniquebodyparts): - pts = self.Dataframe.xs( - (ind, bp), - level=("individuals", "bodyparts"), - axis=1, - ).values - self.circle = patches.Circle( - pts[self.currFrame, :2], - radius=self.markerSize, - fc=self.Colorscheme(ci), - alpha=self.alpha, - ) - self.axes.add_patch(self.circle) - else: - for c, bp in enumerate(self.uniquebodyparts): - pts = self.Dataframe.xs( - (ind, bp), - level=("individuals", "bodyparts"), - axis=1, - ).values - self.circle = patches.Circle( - pts[self.currFrame, :2], - radius=self.markerSize, - fc=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - self.axes.add_patch(self.circle) - col_idx = col_idx + 1 - else: - if self.visualization_rdb.GetSelection() == 0: - for c, bp in enumerate(self.multianimalbodyparts): - pts = self.Dataframe.xs( - (ind, bp), - level=("individuals", "bodyparts"), - axis=1, - ).values - self.circle = patches.Circle( - pts[self.currFrame, :2], - radius=self.markerSize, - fc=self.Colorscheme(ci), - alpha=self.alpha, - ) - self.axes.add_patch(self.circle) - else: - for c, bp in enumerate(self.multianimalbodyparts): - pts = self.Dataframe.xs( - (ind, bp), - level=("individuals", "bodyparts"), - axis=1, - ).values - self.circle = patches.Circle( - pts[self.currFrame, :2], - radius=self.markerSize, - fc=self.Colorscheme(col_idx), - alpha=self.alpha, - ) - self.axes.add_patch(self.circle) - col_idx = col_idx + 1 - self.figure.canvas.draw() - else: - self.norm, self.colorIndex = self.image_panel.getColorIndices( - frame, self.bodyparts - ) - cbar = self.figure.colorbar( - self.ax, cax=cax, spacing="proportional", ticks=self.colorIndex - ) - cbar.set_ticklabels(self.bodyparts) - for bpindex, bp in enumerate(self.bodyparts): - color = self.colormap(self.norm(self.colorIndex[bpindex])) - self.points = [ - self.Dataframe.xs((bp, "x"), level=(-2, -1), axis=1).values[ - self.currFrame - ], - self.Dataframe.xs((bp, "y"), level=(-2, -1), axis=1).values[ - self.currFrame - ], - 1.0, - ] - circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - alpha=self.alpha, - ) - ] - self.axes.add_patch(circle[0]) - self.figure.canvas.draw() - else: - print("Invalid frame") - - def helpButton(self, event): - """ - Opens Instructions - """ - wx.MessageBox( - "1. Use the slider to select a frame in the entire video. \n\n2. Click Grab Frames button to save the specific frame.\ - \n\n3. In the events where you need to extract a range of frames, then use the checkbox 'Range of frames' to select the starting frame index and the number of frames to extract.\ - \n Click the update button to see the frame. Click Grab Frames to select the range of frames. \n\n Click OK to continue", - "Instructions to use!", - wx.OK | wx.ICON_INFORMATION, - ) - - -def show(config, video, shuffle, Dataframe, savelabeled, multianimal): - app = wx.App() - frame = MainFrame( - None, config, video, shuffle, Dataframe, savelabeled, multianimal - ).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "config", "video", "shuffle", "Dataframe", "savelabeled", "multianimal" - ) - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/refine_labels.py b/deeplabcut/gui/refine_labels.py deleted file mode 100644 index 54a815997c..0000000000 --- a/deeplabcut/gui/refine_labels.py +++ /dev/null @@ -1,179 +0,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 -import pydoc -import sys - -import wx - -import deeplabcut - -from deeplabcut.gui import LOGO_PATH -from deeplabcut.utils import auxiliaryfunctions -from pathlib import Path - - -def refine_labels(config, multianimal=False): - """ - 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. - - Parameters - ---------- - config : string - Full path of the config.yaml file as a string. - - Screens : int value of the number of Screens in landscape mode, i.e. if you have 2 screens, enter 2. Default is 1. - - scale_h & scale_w : you can modify how much of the screen the GUI should occupy. The default is .9 and .8, respectively. - - img_scale : if you want to make the plot of the frame larger, consider changing this to .008 or more. Be careful though, too large and you will not see the buttons fully! - - Examples - -------- - >>> deeplabcut.refine_labels('/analysis/project/reaching-task/config.yaml', Screens=2, imag_scale=.0075) - -------- - - """ - - startpath = os.getcwd() - wd = Path(config).resolve().parents[0] - os.chdir(str(wd)) - cfg = auxiliaryfunctions.read_config(config) - if not multianimal and not cfg.get("multianimalproject", False): - from deeplabcut.gui import refinement - - refinement.show(config) - else: # loading multianimal labeling GUI - from deeplabcut.gui import multiple_individuals_refinement_toolbox - - multiple_individuals_refinement_toolbox.show(config) - - os.chdir(startpath) - - -class Refine_labels(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg, page): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - - # variable initilization - self.method = "automatic" - self.config = cfg - self.page = page - # design the panel - sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Step 9. Refine labels") - sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - sizer.Add(icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5) - - line1 = wx.StaticLine(self) - sizer.Add(line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - - sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - self.help_button = wx.Button(self, label="Help") - sizer.Add(self.help_button, pos=(4, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.ok = wx.Button(self, label="LAUNCH") - sizer.Add(self.ok, pos=(4, 4)) - self.ok.Bind(wx.EVT_BUTTON, self.refine_labels) - - self.merge = wx.Button(self, label="Merge dataset") - sizer.Add(self.merge, pos=(4, 3), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.merge.Bind(wx.EVT_BUTTON, self.merge_dataset) - self.merge.Enable(False) - - self.reset = wx.Button(self, label="Reset") - sizer.Add(self.reset, pos=(4, 1), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.reset.Bind(wx.EVT_BUTTON, self.reset_refine_labels) - - sizer.AddGrowableCol(2) - - self.SetSizer(sizer) - sizer.Fit(self) - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.refine_labels" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def refine_labels(self, event): - self.merge.Enable(True) - deeplabcut.refine_labels(self.config) - - def merge_dataset(self, event): - dlg = wx.MessageDialog( - None, - "1. Make sure that you have refined all the labels before merging the dataset.\n\n2. If you merge the dataset, you need to re-create the training dataset before you start the training.\n\n3. Are you ready to merge the dataset?", - "Warning", - wx.YES_NO | wx.ICON_WARNING, - ) - result = dlg.ShowModal() - if result == wx.ID_YES: - notebook = self.GetParent() - notebook.SetSelection(4) - deeplabcut.merge_datasets(self.config, forceiterate=None) - - def reset_refine_labels(self, event): - """ - Reset to default - """ - self.config = [] - self.sel_config.SetPath("") - self.merge.Enable(False) diff --git a/deeplabcut/gui/refine_tracklets.py b/deeplabcut/gui/refine_tracklets.py deleted file mode 100644 index 259b4db23d..0000000000 --- a/deeplabcut/gui/refine_tracklets.py +++ /dev/null @@ -1,345 +0,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 -import pydoc -import sys -import subprocess - -import wx - -import deeplabcut -from deeplabcut.utils import auxiliaryfunctions - -from deeplabcut.gui import LOGO_PATH - - -class Refine_tracklets(wx.Panel): - """ - """ - - def __init__(self, parent, gui_size, cfg): - """Constructor""" - wx.Panel.__init__(self, parent=parent) - self.config = cfg - self.cfg = auxiliaryfunctions.read_config(self.config) - self.datafile = "" - self.video = "" - self.manager = None - self.viz = None - # design the panel - sizer = wx.GridBagSizer(5, 5) - - text = wx.StaticText(self, label="DeepLabCut - Tracklets: Extract/Refine") - sizer.Add(text, pos=(0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15) - # Add logo of DLC - icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(LOGO_PATH)) - sizer.Add(icon, pos=(0, 4), flag=wx.TOP | wx.RIGHT | wx.ALIGN_RIGHT, border=5) - - line1 = wx.StaticLine(self) - sizer.Add(line1, pos=(1, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10) - - self.cfg_text = wx.StaticText(self, label="Select the config file") - sizer.Add(self.cfg_text, pos=(2, 0), flag=wx.TOP | wx.LEFT, border=5) - - if sys.platform == "darwin": - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="*.yaml", - ) - else: - self.sel_config = wx.FilePickerCtrl( - self, - path="", - style=wx.FLP_USE_TEXTCTRL, - message="Choose the config.yaml file", - wildcard="config.yaml", - ) - # self.sel_config = wx.FilePickerCtrl(self, path="",style=wx.FLP_USE_TEXTCTRL,message="Choose the config.yaml file", wildcard="config.yaml") - sizer.Add( - self.sel_config, pos=(2, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_config.SetPath(self.config) - self.sel_config.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_config) - - self.data_text = wx.StaticText(self, label="Select the tracklet pickle file") - sizer.Add(self.data_text, pos=(3, 0), flag=wx.TOP | wx.LEFT, border=5) - self.sel_datafile = wx.FilePickerCtrl( - self, path="", style=wx.FLP_USE_TEXTCTRL, message="Open tracklet data" - ) # wildcard="Pickle files (*.pickle)|*.pickle") - sizer.Add( - self.sel_datafile, - pos=(3, 1), - span=(1, 3), - flag=wx.TOP | wx.EXPAND, - border=5, - ) - self.sel_datafile.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_datafile) - - self.ntracks_text = wx.StaticText(self, label="Number of animals") - sizer.Add(self.ntracks_text, pos=(4, 0), flag=wx.TOP | wx.LEFT, border=5) - self.ntracks = wx.SpinCtrl( - self, value=str(len(self.cfg["individuals"])), min=2, max=1000 - ) - sizer.Add( - self.ntracks, pos=(4, 1), span=(1, 3), flag=wx.EXPAND | wx.TOP, border=5 - ) - - self.create_tracks_btn = wx.Button(self, label="Step1: Create tracks") - sizer.Add(self.create_tracks_btn, pos=(5, 1)) - self.create_tracks_btn.Bind(wx.EVT_BUTTON, self.create_tracks) - - line2 = wx.StaticLine(self) - sizer.Add(line2, pos=(6, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10) - - self.video_text = wx.StaticText(self, label="Select the video") - sizer.Add(self.video_text, pos=(7, 0), flag=wx.TOP | wx.LEFT, border=5) - self.sel_video = wx.FilePickerCtrl( - self, path="", style=wx.FLP_USE_TEXTCTRL, message="Open video" - ) - sizer.Add( - self.sel_video, pos=(7, 1), span=(1, 3), flag=wx.TOP | wx.EXPAND, border=5 - ) - self.sel_video.Bind(wx.EVT_FILEPICKER_CHANGED, self.select_video) - - hbox = wx.BoxSizer(wx.HORIZONTAL) - - slider_swap_text = wx.StaticBox( - self, label="Specify the min swap length to highlight" - ) - slider_swap_sizer = wx.StaticBoxSizer(slider_swap_text, wx.VERTICAL) - self.slider_swap = wx.SpinCtrl(self, value="2") - slider_swap_sizer.Add(self.slider_swap, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - hbox.Add(slider_swap_sizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - sizer.Add( - hbox, pos=(8, 0), flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10 - ) - - hbox_ = wx.BoxSizer(wx.HORIZONTAL) - - slider_gap_text = wx.StaticBox( - self, label="Specify the max gap size of missing data to fill" - ) - slider_gap_sizer = wx.StaticBoxSizer(slider_gap_text, wx.VERTICAL) - self.slider_gap = wx.SpinCtrl(self, value="5") - slider_gap_sizer.Add(self.slider_gap, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - hbox_.Add(slider_gap_sizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - traillength_text = wx.StaticBox(self, label="Trail Length (for visualization)") - traillength_sizer = wx.StaticBoxSizer(traillength_text, wx.VERTICAL) - self.length_track = wx.SpinCtrl(self, value="25") - traillength_sizer.Add(self.length_track, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - hbox_.Add(traillength_sizer, 10, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - - sizer.Add( - hbox_, pos=(9, 0), flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10 - ) - - line3 = wx.StaticLine(self) - sizer.Add( - line3, pos=(10, 0), span=(1, 5), flag=wx.EXPAND | wx.BOTTOM, border=10 - ) - - hbox2 = wx.BoxSizer(wx.HORIZONTAL) - - videotype_text = wx.StaticBox(self, label="Specify the videotype") - videotype_text_boxsizer = wx.StaticBoxSizer(videotype_text, wx.VERTICAL) - videotypes = [".avi", ".mp4", ".mov"] - self.videotype = wx.ComboBox(self, choices=videotypes, style=wx.CB_READONLY) - self.videotype.SetValue(".avi") - videotype_text_boxsizer.Add( - self.videotype, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - shuffle_text = wx.StaticBox(self, label="Specify the shuffle") - shuffle_boxsizer = wx.StaticBoxSizer(shuffle_text, wx.VERTICAL) - self.shuffle = wx.SpinCtrl(self, value="1", min=0, max=100) - shuffle_boxsizer.Add(self.shuffle, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - trainingset = wx.StaticBox(self, label="Specify the trainingset index") - trainingset_boxsizer = wx.StaticBoxSizer(trainingset, wx.VERTICAL) - self.trainingset = wx.SpinCtrl(self, value="0", min=0, max=100) - trainingset_boxsizer.Add( - self.trainingset, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - filter_text = wx.StaticBox(self, label="filter type") - filter_sizer = wx.StaticBoxSizer(filter_text, wx.VERTICAL) - filtertypes = ["median"] - self.filter_track = wx.ComboBox(self, choices=filtertypes, style=wx.CB_READONLY) - self.filter_track.SetValue("median") - filter_sizer.Add(self.filter_track, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) - - filterlength_text = wx.StaticBox(self, label="filter: window length") - filterlength_sizer = wx.StaticBoxSizer(filterlength_text, wx.VERTICAL) - self.filterlength_track = wx.SpinCtrl(self, value="5") - filterlength_sizer.Add( - self.filterlength_track, 20, wx.EXPAND | wx.TOP | wx.BOTTOM, 10 - ) - - hbox2.Add(videotype_text_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox2.Add(shuffle_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox2.Add(trainingset_boxsizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox2.Add(filter_sizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - hbox2.Add(filterlength_sizer, 5, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - sizer.Add( - hbox2, pos=(11, 0), flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10 - ) - - self.inf_cfg_text = wx.Button(self, label="Edit inference_config.yaml") - sizer.Add(self.inf_cfg_text, pos=(12, 2), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.inf_cfg_text.Bind(wx.EVT_BUTTON, self.edit_inf_config) - - self.ok = wx.Button(self, label="Optional: Refine tracks") - sizer.Add(self.ok, pos=(9, 1)) - self.ok.Bind(wx.EVT_BUTTON, self.refine_tracklets) - - self.help_button = wx.Button(self, label="Help") - sizer.Add(self.help_button, pos=(12, 0), flag=wx.LEFT, border=10) - self.help_button.Bind(wx.EVT_BUTTON, self.help_function) - - self.reset = wx.Button(self, label="Reset") - sizer.Add(self.reset, pos=(12, 1), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.reset.Bind(wx.EVT_BUTTON, self.reset_refine_tracklets) - - self.filter = wx.Button( - self, label=" Optional: Filter Tracks (then you also get a CSV file!)" - ) - sizer.Add(self.filter, pos=(11, 1), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.filter.Bind(wx.EVT_BUTTON, self.filter_after_refinement) - - self.export = wx.Button(self, label="Optional: Merge refined data") - sizer.Add(self.export, pos=(13, 1), flag=wx.BOTTOM | wx.RIGHT, border=10) - self.export.Bind(wx.EVT_BUTTON, self.export_data) - self.export.Disable() - - sizer.AddGrowableCol(2) - - self.SetSizer(sizer) - sizer.Fit(self) - - def edit_inf_config(self, event): - # Read the infer config file - trainingsetindex = self.trainingset.GetValue() - trainFraction = self.cfg["TrainingFraction"][trainingsetindex] - self.inf_cfg_path = os.path.join( - self.cfg["project_path"], - auxiliaryfunctions.GetModelFolder( - trainFraction, self.shuffle.GetValue(), self.cfg - ), - "test", - "inference_cfg.yaml", - ) - # let the user open the file with default text editor. Also make it mac compatible - if sys.platform == "darwin": - self.file_open_bool = subprocess.call(["open", self.inf_cfg_path]) - self.file_open_bool = True - else: - import webbrowser - - self.file_open_bool = webbrowser.open(self.inf_cfg_path) - if self.file_open_bool: - self.inf_cfg = auxiliaryfunctions.read_config(self.inf_cfg_path) - else: - raise FileNotFoundError("File not found!") - - def filter_after_refinement(self, event): # why is video type needed? - shuffle = self.shuffle.GetValue() - trainingsetindex = self.trainingset.GetValue() - method = os.path.splitext(self.datafile)[0] - if method.endswith("sk"): - tracker = "skeleton" - elif method.endswith("bx"): - tracker = "box" - else: - tracker = "ellipse" - window_length = self.filterlength_track.GetValue() - if window_length % 2 != 1: - raise ValueError("Window length should be odd.") - - deeplabcut.filterpredictions( - self.config, - [self.video], - videotype=self.videotype.GetValue(), - shuffle=shuffle, - trainingsetindex=trainingsetindex, - filtertype=self.filter_track.GetValue(), - track_method=tracker, - windowlength=self.filterlength_track.GetValue(), - save_as_csv=True, - ) - - def export_data(self, event): - self.viz.export_to_training_data() - - def help_function(self, event): - - filepath = "help.txt" - f = open(filepath, "w") - sys.stdout = f - fnc_name = "deeplabcut.refine_tracklets" - pydoc.help(fnc_name) - f.close() - sys.stdout = sys.__stdout__ - help_file = open("help.txt", "r+") - help_text = help_file.read() - wx.MessageBox(help_text, "Help", wx.OK | wx.ICON_INFORMATION) - help_file.close() - os.remove("help.txt") - - def select_config(self, event): - """ - """ - self.config = self.sel_config.GetPath() - - def select_datafile(self, event): - self.datafile = self.sel_datafile.GetPath() - self.sel_datafile.SetPath(os.path.basename(self.datafile)) - - def select_video(self, event): - self.video = self.sel_video.GetPath() - self.sel_video.SetPath(os.path.basename(self.video)) - - def create_tracks(self, event): - deeplabcut.stitch_tracklets( - self.config, - self.datafile, - n_tracks=self.ntracks.GetValue(), - ) - - def refine_tracklets(self, event): - self.manager, self.viz = deeplabcut.refine_tracklets( - self.config, - self.datafile.replace("pickle", "h5"), - self.video, - min_swap_len=self.slider_swap.GetValue(), - trail_len=self.length_track.GetValue(), - max_gap=self.slider_gap.GetValue(), - ) - self.export.Enable() - - def reset_refine_tracklets(self, event): - """ - Reset to default - """ - self.config = "" - self.datafile = "" - self.video = "" - self.sel_config.SetPath("") - self.sel_datafile.SetPath("") - self.sel_video.SetPath("") - self.slider_swap.SetValue(2) - self.length_track.SetValue(25) - self.slider_gap.SetValue(5) - # self.save.Enable(False) diff --git a/deeplabcut/gui/refinement.py b/deeplabcut/gui/refinement.py deleted file mode 100644 index 304c81bac1..0000000000 --- a/deeplabcut/gui/refinement.py +++ /dev/null @@ -1,903 +0,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 argparse -import os -import os.path -import platform -from pathlib import Path - -# from skimage import io -import PIL -import matplotlib.colors as mcolors -import matplotlib.patches as patches -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import wx -import wx.lib.scrolledpanel as SP -from matplotlib.backends.backend_wxagg import ( - NavigationToolbar2WxAgg as NavigationToolbar, -) -from mpl_toolkits.axes_grid1 import make_axes_locatable -from skimage import io - -from deeplabcut.gui import auxfun_drag -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame -from deeplabcut.utils import auxiliaryfunctions - - -# ########################################################################### -# Class for GUI MainFrame -# ########################################################################### -class ImagePanel(BasePanel): - def drawplot( - self, - img, - img_name, - itr, - index, - threshold, - bodyparts, - cmap, - preview, - keep_view=False, - ): - xlim = self.axes.get_xlim() - ylim = self.axes.get_ylim() - self.axes.clear() - im = io.imread(img) - ax = self.axes.imshow(im, cmap=cmap) - self.orig_xlim = self.axes.get_xlim() - self.orig_ylim = self.axes.get_ylim() - divider = make_axes_locatable(self.axes) - colorIndex = np.linspace(np.min(im), np.max(im), len(bodyparts)) - cax = divider.append_axes("right", size="5%", pad=0.05) - cbar = self.figure.colorbar( - ax, cax=cax, spacing="proportional", ticks=colorIndex - ) - cbar.set_ticklabels(bodyparts[::-1]) - - if not preview: - self.axes.set_title( - str( - str(itr) - + "/" - + str(len(index) - 1) - + " " - + str(Path(index[itr]).stem) - + " " - + " Threshold chosen is: " - + str("{0:.2f}".format(threshold)) - ) - ) - else: - self.axes.set_title( - str( - str(itr) - + "/" - + str(len(index) - 1) - + " " - + str(Path(index[itr]).stem) - ) - ) - - if keep_view: - self.axes.set_xlim(xlim) - self.axes.set_ylim(ylim) - self.figure.canvas.draw() - if not hasattr(self, "toolbar"): - self.toolbar = NavigationToolbar(self.canvas) - return (self.figure, self.axes, self.canvas, self.toolbar) - - def getColorIndices(self, img, bodyparts): - """ - Returns the colormaps ticks and . The order of ticks labels is reversed. - """ - im = io.imread(img) - norm = mcolors.Normalize(vmin=0, vmax=np.max(im)) - ticks = np.linspace(0, np.max(im), len(bodyparts))[::-1] - return norm, ticks - - -class ScrollPanel(SP.ScrolledPanel): - def __init__(self, parent): - SP.ScrolledPanel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) - self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - self.Layout() - - def on_focus(self, event): - pass - - def addCheckBoxSlider(self, bodyparts, fileIndex, markersize): - """ - Adds checkbox and a slider - """ - self.choiceBox = wx.BoxSizer(wx.VERTICAL) - - self.slider = wx.Slider( - self, - -1, - markersize, - 1, - markersize * 3, - size=(250, -1), - style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_LABELS, - ) - self.slider.Enable(False) - self.checkBox = wx.CheckBox(self, id=wx.ID_ANY, label="Adjust marker size.") - self.choiceBox.Add(self.slider, 0, wx.ALL, 5) - self.choiceBox.Add(self.checkBox, 0, wx.ALL, 5) - self.SetSizerAndFit(self.choiceBox) - self.Layout() - return (self.choiceBox, self.slider, self.checkBox) - - def clearBoxer(self): - self.choiceBox.Clear(True) - - -class MainFrame(BaseFrame): - def __init__(self, parent, config): - super(MainFrame, self).__init__("DeepLabCut2.0 - Refinement ToolBox", parent) - self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) - - ################################################################################################################################################### - - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - - topSplitter = wx.SplitterWindow(self) - vSplitter = wx.SplitterWindow(topSplitter) - - self.image_panel = ImagePanel(vSplitter, config, self.gui_size) - self.choice_panel = ScrollPanel(vSplitter) - # self.choice_panel.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False) - # self.choice_panel.SetupScrolling(scroll_x=True, scrollToTop=False) - vSplitter.SplitVertically( - self.image_panel, self.choice_panel, sashPosition=self.gui_size[0] * 0.8 - ) - vSplitter.SetSashGravity(1) - self.widget_panel = WidgetPanel(topSplitter) - topSplitter.SplitHorizontally( - vSplitter, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - self.load = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Load labels") - widgetsizer.Add(self.load, 1, wx.ALL, 15) - self.load.Bind(wx.EVT_BUTTON, self.browseDir) - - self.prev = wx.Button(self.widget_panel, id=wx.ID_ANY, label="< self.iter: - self.updatedCoords = [] - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - # Plotting - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.bodyparts, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - im = io.imread(self.img) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - if np.max(im) == 0: - msg = wx.MessageBox( - "Invalid image. Click Yes to remove", - "Error!", - wx.YES_NO | wx.ICON_WARNING, - ) - if msg == 2: - self.Dataframe = self.Dataframe.drop(self.index[self.iter]) - self.index = list(self.Dataframe.iloc[:, 0].index) - self.iter = self.iter - 1 - - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.bodyparts, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - MainFrame.plot(self, self.img) - else: - self.next.Enable(False) - MainFrame.saveEachImage(self) - - def prevImage(self, event): - """ - Checks the previous Image and enables user to move the annotations. - """ - - MainFrame.saveEachImage(self) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - - self.statusbar.SetStatusText( - "Working on folder: {}".format(os.path.split(str(self.dir))[-1]) - ) - self.next.Enable(True) - self.iter = self.iter - 1 - - # Checks for the first image and disables the Previous button - if self.iter == 0: - self.prev.Enable(False) - - if self.iter >= 0: - self.updatedCoords = [] - # Reading Image - self.img = os.path.join(self.project_path, self.index[self.iter]) - img_name = Path(self.img).name - - # Plotting - self.figure.delaxes( - self.figure.axes[1] - ) # Removes the axes corresponding to the colorbar - ( - self.figure, - self.axes, - self.canvas, - self.toolbar, - ) = self.image_panel.drawplot( - self.img, - img_name, - self.iter, - self.index, - self.threshold, - self.bodyparts, - self.colormap, - self.preview, - keep_view=self.view_locked, - ) - self.axes.callbacks.connect("xlim_changed", self.onZoom) - self.axes.callbacks.connect("ylim_changed", self.onZoom) - MainFrame.plot(self, self.img) - else: - self.prev.Enable(False) - MainFrame.saveEachImage(self) - - def quitButton(self, event): - """ - Quits the GUI - """ - self.statusbar.SetStatusText("") - dlg = wx.MessageDialog( - None, "Are you sure?", "Quit!", wx.YES_NO | wx.ICON_WARNING - ) - result = dlg.ShowModal() - if result == wx.ID_YES: - print( - "Closing... The refined labels are stored in a subdirectory under labeled-data. Use the function 'merge_datasets' to augment the training dataset, and then re-train a network using create_training_dataset followed by train_network!" - ) - self.Destroy() - else: - self.save.Enable(True) - - def helpButton(self, event): - """ - Opens Instructions - """ - self.statusbar.SetStatusText("Help") - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - wx.MessageBox( - "1. Enter the likelihood threshold. \n\n2. All the data points above the threshold will be marked as circle filled with a unique color. All the data points below the threshold will be marked with a hollow circle. \n\n3. Enable the checkbox to adjust the marker size (you will not be able to zoom/pan/home until the next frame). \n\n4. Hover your mouse over data points to see the labels and their likelihood. \n\n5. LEFT click+drag to move the data points. \n\n6. RIGHT click on any data point to remove it. Be careful, you cannot undo this step! \n Click once on the zoom button to zoom-in the image. The cursor will become cross, click and drag over a point to zoom in. \n Click on the zoom button again to disable the zooming function and recover the cursor. \n Use pan button to pan across the image while zoomed in. Use home button to go back to the full default view. \n\n7. When finished click 'Save' to save all the changes. \n\n8. Click OK to continue", - "User instructions", - wx.OK | wx.ICON_INFORMATION, - ) - - def onChecked(self, event): - MainFrame.saveEachImage(self) - self.cb = event.GetEventObject() - if self.cb.GetValue(): - self.slider.Enable(True) - else: - self.slider.Enable(False) - - def check_labels(self): - print("Checking labels if they are outside the image") - for i in self.Dataframe.index: - image_name = os.path.join(self.project_path, i) - im = PIL.Image.open(image_name) - width, height = im.size - for bpindex, bp in enumerate(self.bodyparts): - testCondition = ( - self.Dataframe.loc[i, (self.scorer, bp, "x")] > width - or self.Dataframe.loc[i, (self.scorer, bp, "x")] < 0 - or self.Dataframe.loc[i, (self.scorer, bp, "y")] > height - or self.Dataframe.loc[i, (self.scorer, bp, "y")] < 0 - ) - if testCondition: - print("Found %s outside the image %s.Setting it to NaN" % (bp, i)) - self.Dataframe.loc[i, (self.scorer, bp, "x")] = np.nan - self.Dataframe.loc[i, (self.scorer, bp, "y")] = np.nan - return self.Dataframe - - def saveDataSet(self, event): - - MainFrame.saveEachImage(self) - - # Checks if zoom/pan button is ON - MainFrame.updateZoomPan(self) - self.statusbar.SetStatusText("File saved") - - self.Dataframe = MainFrame.check_labels(self) - # Overwrite machine label file - self.Dataframe.to_hdf(self.dataname, key="df_with_missing", mode="w") - - self.Dataframe.columns.set_levels( - [self.scorer.replace(self.scorer, self.humanscorer)], level=0, inplace=True - ) - self.Dataframe = self.Dataframe.drop("likelihood", axis=1, level=2) - - if Path(self.dir, "CollectedData_" + self.humanscorer + ".h5").is_file(): - print( - "A training dataset file is already found for this video. The refined machine labels are merged to this data!" - ) - DataU1 = pd.read_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5") - ) - # combine datasets Original Col. + corrected machinefiles: - DataCombined = pd.concat([self.Dataframe, DataU1]) - # Now drop redundant ones keeping the first one [this will make sure that the refined machine file gets preference] - DataCombined = DataCombined[~DataCombined.index.duplicated(keep="first")] - """ - if len(self.droppedframes)>0: #i.e. frames were dropped/corrupt. also remove them from original file (if they exist!) - for fn in self.droppedframes: - try: - DataCombined.drop(fn,inplace=True) - except KeyError: - pass - """ - DataCombined.sort_index(inplace=True) - DataCombined.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5"), - key="df_with_missing", - mode="w", - ) - DataCombined.to_csv( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".csv") - ) - else: - self.Dataframe.sort_index(inplace=True) - self.Dataframe.to_hdf( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".h5"), - key="df_with_missing", - mode="w", - ) - self.Dataframe.to_csv( - os.path.join(self.dir, "CollectedData_" + self.humanscorer + ".csv") - ) - self.next.Enable(False) - self.prev.Enable(False) - self.slider.Enable(False) - self.checkBox.Enable(False) - - nextFilemsg = wx.MessageBox( - "File saved. Do you want to refine another file?", - "Repeat?", - wx.YES_NO | wx.ICON_INFORMATION, - ) - if nextFilemsg == 2: - self.file = 1 - # self.buttonCounter = [] - self.updatedCoords = [] - self.dataFrame = None - self.prev.Enable(False) - # self.bodyparts = [] - self.figure.delaxes(self.figure.axes[1]) - self.axes.clear() - self.choiceBox.Clear(True) - MainFrame.updateZoomPan(self) - MainFrame.browseDir(self, event) - - # ########################################################################### - # Other functions - # ########################################################################### - def saveEachImage(self): - """ - Updates the dataframe for the current image with the new datapoints - """ - for bpindex, bp in enumerate(self.bodyparts): - if self.updatedCoords[bpindex]: - self.Dataframe.loc[ - self.Dataframe.index[self.iter], (self.scorer, bp, "x") - ] = self.updatedCoords[bpindex][-1][0] - self.Dataframe.loc[ - self.Dataframe.index[self.iter], (self.scorer, bp, "y") - ] = self.updatedCoords[bpindex][-1][1] - - def getLabels(self, img_index): - """ - Returns a list of x and y labels of the corresponding image index - """ - self.previous_image_points = [] - for bpindex, bp in enumerate(self.bodyparts): - image_points = [ - [ - self.Dataframe[self.scorer][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][bp]["y"].values[self.iter], - bp, - bpindex, - ] - ] - self.previous_image_points.append(image_points) - return self.previous_image_points - - def plot(self, im): - """ - Plots and call auxfun_drag class for moving and removing points. - """ - # small hack in case there are any 0 intensity images! - im = io.imread(im) - maxIntensity = np.max(im) - if maxIntensity == 0: - maxIntensity = np.max(im) + 255 - self.drs = [] - for bpindex, bp in enumerate(self.bodyparts): - color = self.colormap(self.norm(self.colorIndex[bpindex])) - if "CollectedData_" in self.fileName: - self.points = [ - self.Dataframe[self.scorer][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][bp]["y"].values[self.iter], - 1.0, - ] - self.likelihood = self.points[2] - else: - self.points = [ - self.Dataframe[self.scorer][bp]["x"].values[self.iter], - self.Dataframe[self.scorer][bp]["y"].values[self.iter], - self.Dataframe[self.scorer][bp]["likelihood"].values[self.iter], - ] - self.likelihood = self.points[2] - - if self.move2corner: - ny, nx = np.shape(im)[0], np.shape(im)[1] - if self.points[0] > nx or self.points[0] < 0: - self.points[0] = self.center[0] - if self.points[1] > ny or self.points[1] < 0: - self.points[1] = self.center[1] - - if ( - not ("CollectedData_" in self.fileName) - and self.likelihood < self.threshold - ): - circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - facecolor="None", - edgecolor=color, - ) - ] - else: - circle = [ - patches.Circle( - (self.points[0], self.points[1]), - radius=self.markerSize, - fc=color, - alpha=self.alpha, - ) - ] - - self.axes.add_patch(circle[0]) - self.dr = auxfun_drag.DraggablePoint( - circle[0], bp, likelihood=self.likelihood - ) - self.dr.connect() - self.dr.coords = MainFrame.getLabels(self, self.iter)[bpindex] - self.drs.append(self.dr) - self.updatedCoords.append(self.dr.coords) - self.figure.canvas.draw() - - -def show(config): - app = wx.App() - frame = MainFrame(None, config).Show() - app.MainLoop() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/select_crop_parameters.py b/deeplabcut/gui/select_crop_parameters.py deleted file mode 100644 index 1c7d23b4c4..0000000000 --- a/deeplabcut/gui/select_crop_parameters.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" - -import argparse - -import wx -from matplotlib.figure import Figure -from matplotlib.widgets import RectangleSelector -from deeplabcut.gui.widgets import BasePanel, WidgetPanel, BaseFrame - - -class MainFrame(BaseFrame): - def __init__(self, parent, config, image): - super(MainFrame, self).__init__( - "DeepLabCut2.0 - Select Crop Parameters", parent - ) - - ################################################################################################################################################### - # Spliting the frame into top and bottom panels. Bottom panels contains the widgets. The top panel is for showing images and plotting! - topSplitter = wx.SplitterWindow(self) - - self.image_panel = BasePanel(topSplitter, config, self.gui_size) - self.widget_panel = WidgetPanel(topSplitter) - - topSplitter.SplitHorizontally( - self.image_panel, self.widget_panel, sashPosition=self.gui_size[1] * 0.83 - ) # 0.9 - topSplitter.SetSashGravity(1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(topSplitter, 1, wx.EXPAND) - self.SetSizer(sizer) - - ################################################################################################################################################### - # Add Buttons to the WidgetPanel and bind them to their respective functions. - - widgetsizer = wx.WrapSizer(orient=wx.HORIZONTAL) - - self.help = wx.Button(self.widget_panel, id=wx.ID_ANY, label="Help") - widgetsizer.Add(self.help, 1, wx.ALL, 15) - self.help.Bind(wx.EVT_BUTTON, self.helpButton) - - self.quit = wx.Button( - self.widget_panel, id=wx.ID_ANY, label="Save parameters and Quit" - ) - widgetsizer.Add(self.quit, 1, wx.ALL, 15) - self.quit.Bind(wx.EVT_BUTTON, self.quitButton) - - self.widget_panel.SetSizer(widgetsizer) - self.widget_panel.SetSizerAndFit(widgetsizer) - self.widget_panel.Layout() - - # Variables initialization - self.image = image - self.coords = [] - self.figure = Figure() - self.axes = self.figure.add_subplot(111) - # self.cfg = auxiliaryfunctions.read_config(config) - MainFrame.show_image(self) - - def quitButton(self, event): - """ - Quits the GUI - """ - # self.statusbar.SetStatusText("") - # dlg = wx.MessageDialog(None,"Are you sure?", "Quit!",wx.YES_NO | wx.ICON_WARNING) - # result = dlg.ShowModal() - # if result == wx.ID_YES: - self.Destroy() - - def show_image(self): - self.figure, self.axes, self.canvas = self.image_panel.getfigure() - self.ax = self.axes.imshow(self.image) - self.figure.canvas.draw() - self.cid = RectangleSelector( - self.axes, - self.line_select_callback, - drawtype="box", - useblit=False, - button=[1], - minspanx=5, - minspany=5, - spancoords="pixels", - interactive=True, - ) - self.canvas.mpl_connect("key_press_event", self.cid) - - def line_select_callback(self, eclick, erelease): - "eclick and erelease are the press and release events" - new_x1, new_y1 = eclick.xdata, eclick.ydata - new_x2, new_y2 = erelease.xdata, erelease.ydata - coords = [ - str(int(new_x1)), - str(int(new_x2)), - str(int(new_y1)), - str(int(new_y2)), - ] - self.coords = coords - - def helpButton(self, event): - """ - Opens Instructions - """ - wx.MessageBox( - "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 " - "Save parameters and Quit" - " to save the croppeing parameters and close the GUI. \n\n Click OK to continue", - "Instructions to use!", - wx.OK | wx.ICON_INFORMATION, - ) - - -def show(config, image): - app = wx.App() - main = MainFrame(None, config, image) - main.Show() - app.MainLoop() - return main.coords - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("config", "image") - cli_args = parser.parse_args() diff --git a/deeplabcut/gui/tabs/__init__.py b/deeplabcut/gui/tabs/__init__.py new file mode 100644 index 0000000000..c7f7f534fb --- /dev/null +++ b/deeplabcut/gui/tabs/__init__.py @@ -0,0 +1,25 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from deeplabcut.gui.tabs.analyze_videos import AnalyzeVideos +from deeplabcut.gui.tabs.create_project import ProjectCreator +from deeplabcut.gui.tabs.create_training_dataset import CreateTrainingDataset +from deeplabcut.gui.tabs.create_videos import CreateVideos +from deeplabcut.gui.tabs.evaluate_network import EvaluateNetwork +from deeplabcut.gui.tabs.extract_frames import ExtractFrames +from deeplabcut.gui.tabs.extract_outlier_frames import ExtractOutlierFrames +from deeplabcut.gui.tabs.label_frames import LabelFrames +from deeplabcut.gui.tabs.manage_project import ManageProject +from deeplabcut.gui.tabs.modelzoo import ModelZoo +from deeplabcut.gui.tabs.open_project import OpenProject +from deeplabcut.gui.tabs.refine_tracklets import RefineTracklets +from deeplabcut.gui.tabs.train_network import TrainNetwork +from deeplabcut.gui.tabs.unsupervised_id_tracking import UnsupervizedIdTracking +from deeplabcut.gui.tabs.video_editor import VideoEditor diff --git a/deeplabcut/gui/tabs/analyze_videos.py b/deeplabcut/gui/tabs/analyze_videos.py new file mode 100644 index 0000000000..0a3a448ae4 --- /dev/null +++ b/deeplabcut/gui/tabs/analyze_videos.py @@ -0,0 +1,456 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from dataclasses import dataclass +from functools import partial +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt, QTimer + +import deeplabcut +from deeplabcut.core.config import ProjectConfig +from deeplabcut.gui.components import ( + BodypartListWidget, + DefaultTab, + ShuffleSpinBox, + VideoSelectionWidget, + _create_grid_layout, + _create_horizontal_layout, + _create_label_widget, + _create_vertical_layout, +) +from deeplabcut.gui.utils import move_to_separate_thread +from deeplabcut.gui.widgets import ConfigEditor + + +@dataclass(frozen=True) +class AnalyzeVideosOptions: + config_path: str | Path + shuffle: int + save_as_csv: bool + filter_data: bool + plot_trajectories: bool + show_trajectory_plots: bool + displayed_bodyparts: tuple[str, ...] + create_video_all_detections: bool + auto_track: bool + calibrate_assembly: bool + assemble_with_ID_only: bool + num_animals_in_videos: int | None + cropping: tuple[int, int, int, int] | None + dynamic_cropping_params: tuple[bool, float, int] + track_method: str | None + + +class AnalyzeVideos(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self._reload_timer = QTimer(self) + self._reload_timer.setSingleShot(True) + self._reload_timer.setInterval(0) + self._reload_timer.timeout.connect(self.root.reload_project_config) + + self._set_page() + + @property + def files(self): + return self.video_selection_widget.files + + def _set_page(self): + self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) + self.video_selection_widget = VideoSelectionWidget( + self.root, self, hide_videotype=True, sync_videotype_with_selection=True + ) + self.main_layout.addWidget(self.video_selection_widget) + + tmp_layout = _create_horizontal_layout() + + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_grid_layout() + self._generate_layout_attributes(self.layout_attributes) + tmp_layout.addLayout(self.layout_attributes) + + # Single / Multi animal Only Layouts + self.layout_singleanimal = _create_horizontal_layout() + self.layout_multianimal = _create_horizontal_layout() + + if self.root.is_multianimal: + self._generate_layout_multianimal(self.layout_multianimal) + tmp_layout.addLayout(self.layout_multianimal) + else: + self._generate_layout_single_animal(self.layout_singleanimal) + tmp_layout.addLayout(self.layout_singleanimal) + + self.main_layout.addLayout(tmp_layout) + + self.main_layout.addWidget(_create_label_widget("", "font:bold")) + self.layout_other_options = _create_vertical_layout() + self._generate_layout_other_options(self.layout_other_options) + self.main_layout.addLayout(self.layout_other_options) + + self.analyze_videos_btn = QtWidgets.QPushButton("Analyze Videos") + self.analyze_videos_btn.clicked.connect(self.analyze_videos) + + self.edit_config_file_btn = QtWidgets.QPushButton("Edit config.yaml") + self.edit_config_file_btn.clicked.connect(self.edit_config_file) + + self.main_layout.addWidget(self.analyze_videos_btn, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.edit_config_file_btn, alignment=Qt.AlignRight) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel(deeplabcut.analyze_videos.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_single_animal(self, layout): + # Dynamic bodypart cropping + self.crop_bodyparts = QtWidgets.QCheckBox("Dynamically crop bodyparts") + self.crop_bodyparts.setCheckState(Qt.Unchecked) + self.crop_bodyparts.stateChanged.connect(self.update_crop_choice) + + self.dynamic_cropping = False + layout.addWidget(self.crop_bodyparts) + + def _generate_layout_other_options(self, layout): + tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) + + # Save results as csv + self.save_as_csv = QtWidgets.QCheckBox("Save result(s) as csv") + self.save_as_csv.setCheckState(Qt.Unchecked) + self.save_as_csv.stateChanged.connect(self.update_csv_choice) + + tmp_layout.addWidget(self.save_as_csv) + + # Filter predictions + self.filter_predictions = QtWidgets.QCheckBox("Filter predictions") + self.filter_predictions.setCheckState(Qt.Unchecked) + self.filter_predictions.stateChanged.connect(self.update_filter_choice) + + tmp_layout.addWidget(self.filter_predictions) + + # Plot Trajectories + self.plot_trajectories = QtWidgets.QCheckBox("Plot trajectories") + self.plot_trajectories.setCheckState(Qt.Unchecked) + self.plot_trajectories.stateChanged.connect(self.update_plot_trajectory_choice) + + tmp_layout.addWidget(self.plot_trajectories) + + # Show trajectory plots + self.show_trajectory_plots = QtWidgets.QCheckBox("Show trajectory plots") + self.show_trajectory_plots.setCheckState(Qt.Unchecked) + self.show_trajectory_plots.setEnabled(False) + self.show_trajectory_plots.stateChanged.connect(self.update_showfigs_choice) + + tmp_layout.addWidget(self.show_trajectory_plots) + + layout.addLayout(tmp_layout) + + self.bodyparts_list_widget = BodypartListWidget(root=self.root, parent=self) + layout.addWidget(self.bodyparts_list_widget, Qt.AlignLeft) + + def _generate_layout_attributes(self, layout): + # Shuffle + opt_text = QtWidgets.QLabel("Shuffle") + self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + + layout.addWidget(opt_text, 0, 0) + layout.addWidget(self.shuffle, 0, 1) + + def _generate_layout_multianimal(self, layout): + tmp_layout = QtWidgets.QGridLayout() + + opt_text = QtWidgets.QLabel("Tracking method") + self.tracker_type_widget = QtWidgets.QComboBox() + self.tracker_type_widget.addItems(["ellipse", "box", "skeleton"]) + self.tracker_type_widget.currentTextChanged.connect(self.update_tracker_type) + tmp_layout.addWidget(opt_text, 0, 0) + tmp_layout.addWidget(self.tracker_type_widget, 0, 1) + + opt_text = QtWidgets.QLabel("Number of animals in videos") + self.num_animals_in_videos = QtWidgets.QSpinBox() + self.num_animals_in_videos.setMaximum(100) + self.num_animals_in_videos.setValue(len(self.root.all_individuals)) + tmp_layout.addWidget(opt_text, 1, 0) + tmp_layout.addWidget(self.num_animals_in_videos, 1, 1) + + # layout.addLayout(tmp_layout) + + # tmp_layout = QtWidgets.QGridLayout() + + 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) + 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.setCheckState(Qt.Unchecked) + 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.setCheckState(Qt.Unchecked) + 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) + + def update_create_video_detections(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Create video with all detections {s}") + + def update_assemble_with_ID_only(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Assembly with ID only {s}") + + def update_calibrate_assembly(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Assembly calibration {s}") + + def update_tracker_type(self, method): + self.root.logger.info(f"Using {method.upper()} tracker") + + def update_csv_choice(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Save results as CSV {s}") + + def update_filter_choice(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Filtering predictions {s}") + + def update_showfigs_choice(self, state): + if Qt.CheckState(state) == Qt.Checked: + self.root.logger.info("Plots will show as pop ups.") + else: + self.root.logger.info("Plots will not show up.") + + def update_crop_choice(self, state): + if Qt.CheckState(state) == Qt.Checked: + self.root.logger.info("Dynamic bodypart cropping ENABLED.") + self.dynamic_cropping = True + else: + self.root.logger.info("Dynamic bodypart cropping DISABLED.") + self.dynamic_cropping = False + + def update_plot_trajectory_choice(self, state): + if Qt.CheckState(state) == Qt.Checked: + self.bodyparts_list_widget.refresh() + self.bodyparts_list_widget.show() + self.bodyparts_list_widget.setEnabled(True) + self.show_trajectory_plots.setEnabled(True) + self.root.logger.info("Plot trajectories ENABLED.") + + else: + self.bodyparts_list_widget.hide() + self.bodyparts_list_widget.setEnabled(False) + self.show_trajectory_plots.setEnabled(False) + self.show_trajectory_plots.setCheckState(Qt.Unchecked) + self.root.logger.info("Plot trajectories DISABLED.") + + def edit_config_file(self): + if not self.root.config_path: + return + config = self.root.config_path + editor = ConfigEditor(config, parent=self.root) + editor.accepted.connect(self._reload_timer.start) + editor.show() + + def _collect_options(self) -> AnalyzeVideosOptions: + config_path = self.root.config_path + shuffle = self.root.shuffle_value + save_as_csv = self.save_as_csv.isChecked() + filter_data = self.filter_predictions.isChecked() + plot_trajectories = self.plot_trajectories.isChecked() + show_trajectory_plots = self.show_trajectory_plots.isChecked() + displayed_bodyparts = tuple(self.bodyparts_list_widget.selected_bodyparts) if plot_trajectories else () + + if self.root.is_multianimal: + calibrate_assembly = self.calibrate_assembly_checkbox.isChecked() + assemble_with_ID_only = self.assemble_with_ID_only_checkbox.isChecked() + track_method = self.tracker_type_widget.currentText() + num_animals_in_videos = self.num_animals_in_videos.value() + create_video_all_detections = self.create_detections_video_checkbox.isChecked() + else: + calibrate_assembly = False + assemble_with_ID_only = False + track_method = None + num_animals_in_videos = None + create_video_all_detections = False + + cropping = None + crop_flag = self.root.cfg.get("cropping", False) + if str(crop_flag).lower() == "true": + cropping = ( + self.root.cfg["x1"], + self.root.cfg["x2"], + self.root.cfg["y1"], + self.root.cfg["y2"], + ) + + dynamic_cropping_params = (False, 0.5, 10) + if getattr(self, "dynamic_cropping", False): + dynamic_cropping_params = (True, 0.5, 10) + + return AnalyzeVideosOptions( + config_path=config_path, + shuffle=shuffle, + save_as_csv=save_as_csv, + filter_data=filter_data, + plot_trajectories=plot_trajectories, + show_trajectory_plots=show_trajectory_plots, + displayed_bodyparts=displayed_bodyparts, + create_video_all_detections=create_video_all_detections, + auto_track=self.root.is_multianimal, + calibrate_assembly=calibrate_assembly, + assemble_with_ID_only=assemble_with_ID_only, + num_animals_in_videos=num_animals_in_videos, + cropping=cropping, + dynamic_cropping_params=dynamic_cropping_params, + track_method=track_method, + ) + + def _get_video_batches(self): + """ + Returns a list of (videotype, videos) pairs. + videotype should include the leading dot, e.g. '.avi'. + """ + groups = self.video_selection_widget.get_files_grouped_by_suffix(keep_dot=True) + batches = [(suffix, videos) for suffix, videos in sorted(groups.items()) if suffix] + return batches + + def _get_unique_video_parent_folders(self, batches: list[tuple[str, list[Path]]]) -> list[Path]: + folders = [] + seen = set() + + for _, videos in batches: + for video in videos: + parent = video.parent.absolute() + if parent not in seen: + seen.add(parent) + folders.append(parent) + + return folders + + def _run_pipeline(self, options: AnalyzeVideosOptions, batches: list[tuple[str, list[Path]]]): + for videotype, videos in batches: + try: + self.root.logger.info(f"Analyzing {len(videos)} video(s) with extension {videotype}") + + deeplabcut.analyze_videos( + options.config_path, + videos=videos, + video_extensions=videotype, + shuffle=options.shuffle, + save_as_csv=options.save_as_csv, + cropping=options.cropping, + dynamic=options.dynamic_cropping_params, + auto_track=options.auto_track, + n_tracks=options.num_animals_in_videos, + calibrate=options.calibrate_assembly, + identity_only=options.assemble_with_ID_only, + ) + + self._run_postprocessing_for_group(options, videotype, videos) + except Exception as e: + exc = f"Error analyzing videos {videos} with extension {videotype}: {e}" + self.root.logger.error(exc, exc_info=True) + raise RuntimeError(exc) from e + + # Run CSV conversion once per unique folder, after all batches + if options.auto_track and options.save_as_csv: + self._convert_outputs_to_csv_once_per_folder(batches) + + def _run_postprocessing_for_group( + self, + options: AnalyzeVideosOptions, + videotype: str, + videos: list[str], + ): + if options.create_video_all_detections: + deeplabcut.create_video_with_all_detections( + options.config_path, + videos=videos, + video_extensions=videotype, + shuffle=options.shuffle, + ) + + if options.filter_data: + deeplabcut.filterpredictions( + options.config_path, + video=videos, + video_extensions=videotype, + shuffle=options.shuffle, + filtertype="median", + windowlength=5, + save_as_csv=options.save_as_csv, + track_method=options.track_method, + ) + + if options.plot_trajectories: + deeplabcut.plot_trajectories( + options.config_path, + videos=videos, + displayedbodyparts=options.displayed_bodyparts, + video_extensions=videotype, + shuffle=options.shuffle, + filtered=options.filter_data, + showfigures=options.show_trajectory_plots, + track_method=options.track_method, + ) + + def _convert_outputs_to_csv_once_per_folder(self, batches: list[tuple[str, list[Path]]]): + folders = self._get_unique_video_parent_folders(batches) + + for folder in folders: + self.root.logger.info(f"Converting H5 outputs to CSV in folder: {folder}") + deeplabcut.analyze_videos_converth5_to_csv( + folder, + listofvideos=False, + ) + + def analyze_videos(self): + options = self._collect_options() + batches = self._get_video_batches() + + if not batches: + self.root.logger.warning("No videos selected.") + return + + # Keep config in sync with GUI choice before launching worker + if self.root.is_multianimal and options.track_method is not None: + # NOTE @deruyter92 2026-06-23: Most of GUI is not migrated to typed configs yet, so we normalize here. + cfg = ProjectConfig.from_any(self.root.config_path, repair_path=True) + + # Update track method and write to disk + cfg.default_track_method = options.track_method + cfg.to_yaml(self.root.config_path, overwrite=True, log_changes=True, mark_clean=True) + + func = partial(self._run_pipeline, options, batches) + + self.worker, self.thread = move_to_separate_thread(func) + self.worker.error.connect(self.root.show_task_error) + self.worker.finished.connect(lambda: self.analyze_videos_btn.setEnabled(True)) + self.worker.finished.connect(lambda: self.root._progress_bar.hide()) + self.thread.start() + self.analyze_videos_btn.setEnabled(False) + self.root._progress_bar.show() diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py new file mode 100644 index 0000000000..3450f40897 --- /dev/null +++ b/deeplabcut/gui/tabs/create_project.py @@ -0,0 +1,540 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import os +from datetime import datetime +from pathlib import Path + +from PySide6 import QtCore, QtWidgets +from PySide6.QtGui import QBrush, QColor, QDesktopServices, QPainter, QPen + +from deeplabcut.create_project import create_new_project, create_new_project_3d +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.gui_assets import icon_from_resource +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 +from deeplabcut.utils.auxfun_videos import collect_video_paths + + +class DynamicTextList(QtWidgets.QWidget): + """Dynamically add text entries.""" + + def __init__(self, label_text="bodyparts", parent=None): + super().__init__(parent) + self.label_text = label_text + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + # Set maximum width for the widget + self.setMaximumWidth(300) + + # Add explanatory label + label = QtWidgets.QLabel(label_text) + self.layout.addWidget(label) + + # Create scroll area and its widget + self.scroll = QtWidgets.QScrollArea() + self.scroll.setWidgetResizable(True) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.scroll.setFrameShape(QtWidgets.QFrame.NoFrame) # Remove frame border + + # Create widget to hold the entries + self.entries_widget = QtWidgets.QWidget() + self.entries_layout = QtWidgets.QVBoxLayout(self.entries_widget) + self.entries_layout.setContentsMargins(0, 0, 0, 0) + self.entries_layout.setSpacing(5) # Consistent spacing between entries + self.entries_layout.setAlignment(QtCore.Qt.AlignTop) # Align entries to top + + # Add stretch at the bottom to keep entries at top + self.entries_layout.addStretch() + + self.scroll.setWidget(self.entries_widget) + + # Set fixed height for 6 items + self.entry_height = 30 # Fixed height for each entry + self.padding = 10 # Extra padding + self.scroll.setFixedHeight(5 * self.entry_height + self.padding) + + # Add scroll area to main layout + self.layout.addWidget(self.scroll) + + self.entries = [] + self.add_entry() + + def add_entry(self): + # Create horizontal layout for index and entry + entry_layout = QtWidgets.QHBoxLayout() + entry_layout.setContentsMargins(0, 0, 10, 0) + entry_layout.setSpacing(5) # Consistent spacing between index and entry + + # Create container widget for the entry row + entry_widget = QtWidgets.QWidget() + entry_widget.setFixedHeight(self.entry_height) + entry_widget.setLayout(entry_layout) + + # Add index label + index_label = QtWidgets.QLabel(str(len(self.entries) + 1) + ".") + index_label.setFixedWidth(20) # Set fixed width for alignment + entry_layout.addWidget(index_label) + + # Add text entry + entry = QtWidgets.QLineEdit() + entry.setFixedHeight(self.entry_height - 6) # Slightly smaller than container + entry.textChanged.connect(self._on_text_changed) + entry.textEdited.connect(lambda text: self._check_for_spaces(entry, text)) + self.entries.append((entry, index_label)) # Store both widgets + entry_layout.addWidget(entry) + + # Insert the new entry before the stretch + self.entries_layout.insertWidget(len(self.entries) - 1, entry_widget) + + 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 instead.") + msg.setWindowTitle("Warning") + msg.exec_() + entry.setText(entry.text().replace(" ", "_")) + + def _on_text_changed(self): + # If the last entry has text, add a new empty entry + if self.entries[-1][0].text(): + self.add_entry() + + # Remove any empty entries except the last one + entries_to_remove = [] + for i, (entry, _) in enumerate(self.entries[:-1]): + if not entry.text(): + entries_to_remove.append(i) + + for i in reversed(entries_to_remove): + entry_widget = self.entries[i][0].parent() + self.entries_layout.removeWidget(entry_widget) + entry_widget.deleteLater() + self.entries.pop(i) + + self._update_indices() # Update the indices after removal + + 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): + index_label.setText(str(i + 1) + ".") + + +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 + self.off_text = off_text + self.setCheckable(True) + self.setFixedWidth(width) + self.setMinimumHeight(22) + + def paintEvent(self, event): + # Colors: https://qdarkstylesheet.readthedocs.io/en/latest/color_reference.html + label = self.on_text if self.isChecked() else self.off_text + bg_color = "#00ff00" if self.isChecked() else "#9DA9B5" + + radius = 10 + width = 32 + center = self.rect().center() + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.translate(center) + painter.setBrush(QColor(69, 83, 100)) # Lighter gray background + + pen = QPen("#455364") + pen.setWidth(2) + painter.setPen(pen) + + 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(): + sw_rect.moveLeft(-width) + + painter.drawRoundedRect(sw_rect, radius, radius) + + pen = QPen("#000000") + pen.setWidth(2) + painter.setPen(pen) + painter.drawText(sw_rect, QtCore.Qt.AlignCenter, label) + + +class ProjectCreator(QtWidgets.QDialog): + """Project creation dialog.""" + + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.setWindowTitle("New Project") + self.setModal(True) + self.setMinimumWidth(parent.screen_width // 2) + today = datetime.today().strftime("%Y-%m-%d") + self.name_default = "-".join(("{}", "{}", today)) + self.proj_default = "" + self.exp_default = "" + self.loc_default = parent.project_folder + + self.bodypart_list = None + self.individuals_list = None + self.unique_bodyparts_list = None + + self.toggle_3d = Switch() + self.toggle_3d.setChecked(False) + self.madlc_toggle = Switch() + self.madlc_toggle.setChecked(False) + self.unique_toggle = Switch() + self.unique_toggle.setChecked(False) + self.identity_toggle = Switch() + self.identity_toggle.setChecked(False) + + main_layout = QtWidgets.QVBoxLayout(self) + self.user_frame = self.lay_out_user_frame() + self.video_frame = self.lay_out_video_frame() + self.create_button = QtWidgets.QPushButton("Create") + self.create_button.setDefault(True) + self.create_button.clicked.connect(self.finalize_project) + main_layout.addWidget(self.user_frame) + main_layout.addWidget(self.video_frame) + main_layout.addWidget(self.create_button, alignment=QtCore.Qt.AlignRight) + + def lay_out_user_frame(self): + user_frame = QtWidgets.QFrame(self) + user_frame.setFrameShape(user_frame.Shape.StyledPanel) + user_frame.setLineWidth(0) + + proj_label = QtWidgets.QLabel("Project:", user_frame) + self.proj_line = QtWidgets.QLineEdit(self.proj_default, user_frame) + self.proj_line.setPlaceholderText("my project's name") + self._default_style = self.proj_line.styleSheet() + self.proj_line.textEdited.connect(self.update_project_name) + + exp_label = QtWidgets.QLabel("Experimenter:", user_frame) + self.exp_line = QtWidgets.QLineEdit(self.exp_default, user_frame) + self.exp_line.setPlaceholderText("my nickname") + self.exp_line.textEdited.connect(self.update_experimenter_name) + + loc_label = ClickableLabel("Location:", parent=user_frame) + loc_label.signal.connect(self.on_click) + self.loc_line = QtWidgets.QLineEdit(os.fspath(self.loc_default), user_frame) + self.loc_line.setReadOnly(True) + action = self.loc_line.addAction( + icon_from_resource("icons", "open2.png"), + QtWidgets.QLineEdit.TrailingPosition, + ) + action.triggered.connect(self.on_click) + + vbox = QtWidgets.QVBoxLayout(user_frame) + grid = QtWidgets.QGridLayout() + grid.addWidget(proj_label, 0, 0) + grid.addWidget(self.proj_line, 0, 1) + grid.addWidget(exp_label, 1, 0) + grid.addWidget(self.exp_line, 1, 1) + grid.addWidget(loc_label, 2, 0) + grid.addWidget(self.loc_line, 2, 1) + vbox.addLayout(grid) + + widget_3d = self.build_toggle_widget( + switch=self.toggle_3d, + question="Do you want to create a 3D pose estimation project?", + help_text="(What is needed for a 3D project?)", + docs_link=URL_3D, + ) + madlc_widget = self.build_toggle_widget( + switch=self.madlc_toggle, + question="Are there multiple individuals in your videos?", + help_text="(Why does this matter?)", + docs_link=URL_USE_GUIDE_SCENARIO, + ) + + # Only visible when the maDLC widget is checked + unique_widget = self.build_toggle_widget( + switch=self.unique_toggle, + question="Do you have unique bodyparts in your video?", + help_text="(What are unique bodyparts?)", + docs_link=URL_MA_CONFIGURE, + ) + unique_widget.setVisible(False) + + # Labelling with identity + identity_widget = self.build_toggle_widget( + switch=self.identity_toggle, + question="Label with identity?", + help_text="(What is labeling with identity?)", + docs_link=URL_MA_CONFIGURE, + ) + identity_widget.setVisible(False) + + vbox.addWidget(widget_3d, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(madlc_widget, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(unique_widget, alignment=QtCore.Qt.AlignTop) + vbox.addWidget(identity_widget, alignment=QtCore.Qt.AlignTop) + + # Create horizontal layout for the two lists + lists_layout = QtWidgets.QHBoxLayout() + lists_layout.setAlignment(QtCore.Qt.AlignTop) + + # Create both DynamicTextList widgets as class attributes + self.bodypart_list = DynamicTextList( + label_text="Bodyparts to track", + parent=self, + ) + + self.individuals_list = DynamicTextList( + label_text="Individual names", + parent=self, + ) + self.individuals_list.setVisible(False) + + self.unique_bodyparts_list = DynamicTextList( + label_text="Unique bodyparts to track", + parent=self, + ) + self.unique_bodyparts_list.setVisible(False) + + # Connect toggle state to individuals list visibility, unique, identity + self.madlc_toggle.toggled.connect(self.individuals_list.setVisible) + self.madlc_toggle.toggled.connect(unique_widget.setVisible) + self.madlc_toggle.toggled.connect(identity_widget.setVisible) + + # 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()) + ) + + # 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()) + ) + 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()) + ) + 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() + ) + ) + + # Add both lists to the horizontal layout with top alignment + lists_layout.addWidget(self.bodypart_list, alignment=QtCore.Qt.AlignTop) + lists_layout.addWidget(self.individuals_list, alignment=QtCore.Qt.AlignTop) + lists_layout.addWidget(self.unique_bodyparts_list, alignment=QtCore.Qt.AlignTop) + + # Add the horizontal layout to the main vertical layout + vbox.addLayout(lists_layout) + return user_frame + + def build_toggle_widget( + self, + switch: Switch, + question: str, + help_text: str, + docs_link: str, + ) -> QtWidgets.QWidget: + toggle_layout = QtWidgets.QHBoxLayout() + toggle_layout.setContentsMargins(0, 0, 0, 0) + toggle_layout.setSpacing(10) + + toggle_label = QtWidgets.QLabel(question) + toggle_label.setAlignment(QtCore.Qt.AlignLeft) + 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))) + + toggle_layout.addWidget(switch, alignment=QtCore.Qt.AlignLeft) + toggle_layout.addWidget(toggle_label, alignment=QtCore.Qt.AlignLeft) + toggle_layout.addStretch() + toggle_layout.addWidget(help_label, alignment=QtCore.Qt.AlignRight) + toggle_widget = QtWidgets.QWidget() + toggle_widget.setLayout(toggle_layout) + return toggle_widget + + def lay_out_video_frame(self): + video_frame = ItemSelectionFrame([], self) + + self.copy_box = QtWidgets.QCheckBox("Copy videos to project folder") + self.copy_box.setChecked(False) + + # Add checkbox for selecting individual files + self.select_files_box = QtWidgets.QCheckBox("Select individual files") + self.select_files_box.setChecked(False) + + browse_button = QtWidgets.QPushButton("Browse for videos") + browse_button.clicked.connect(self.browse_videos) + clear_button = QtWidgets.QPushButton("Clear") + clear_button.clicked.connect(video_frame.fancy_list.clear) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(browse_button) + layout.addWidget(clear_button) + video_frame.layout.addLayout(layout) + video_frame.layout.addWidget(self.copy_box) + video_frame.layout.addWidget(self.select_files_box) + + self.toggle_3d.toggled.connect(lambda yes: self.copy_box.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: browse_button.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: clear_button.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: video_frame.setVisible(not yes)) + self.toggle_3d.toggled.connect(lambda yes: self.select_files_box.setVisible(not yes)) + return video_frame + + 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", + os.fspath(self.loc_default), + video_filter, + options=options, + ) + + if files: + for video in files: + self.video_frame.fancy_list.add_item(video) + else: + # Browse folders for videos + folder = QtWidgets.QFileDialog.getExistingDirectory( + self, + "Please select a folder", + os.fspath(self.loc_default), + options, + ) + if not folder: + return + + for video in collect_video_paths(folder): + if video.suffix[1:].lower() in DLCParams.VIDEOTYPES[1:]: + self.video_frame.fancy_list.add_item(str(video)) + + def finalize_project(self): + fields = [self.proj_line, self.exp_line] + empty = [i for i, field in enumerate(fields) if not field.text()] + for i, field in enumerate(fields): + if i in empty: + field.setStyleSheet("border: 1px solid red;") + else: + field.setStyleSheet(self._default_style) + if empty: + return + + create_3d = self.toggle_3d.isChecked() + try: + if create_3d: + _ = create_new_project_3d( + self.proj_default, + self.exp_default, + 2, + self.loc_default, + ) + else: + videos = list(self.video_frame.selected_items) + if not len(videos): + print("Add at least a video to the project.") + 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) + to_copy = self.copy_box.isChecked() + is_madlc = self.madlc_toggle.isChecked() + config = create_new_project( + self.proj_default, + self.exp_default, + videos, + self.loc_default, + to_copy, + multianimal=is_madlc, + ) + + if self.bodypart_list is not None: + bodypart_key = "bodyparts" + updates = {} + if is_madlc: + bodypart_key = "multianimalbodyparts" + if self.individuals_list is not None: + individuals = self.individuals_list.get_entries() + if len(individuals) > 0: + updates["individuals"] = individuals + + 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 + + if self.identity_toggle.isChecked(): + updates["identity"] = True + + bodyparts = self.bodypart_list.get_entries() + if len(bodyparts) > 0: + updates[bodypart_key] = bodyparts + + if len(updates) > 0: + cfg: dict = auxiliaryfunctions.read_config(config) + cfg.update(**updates) + auxiliaryfunctions.write_config(config, cfg) + + self.parent.load_config(config) + self.parent._update_project_state(config=config, loaded=True) + except FileExistsError: + print(f'Project "{self.proj_default}" already exists!') + return + + msg = QtWidgets.QMessageBox(text="New project created") + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.exec_() + + self.close() + + def on_click(self): + dirname = QtWidgets.QFileDialog.getExistingDirectory( + self, "Please select a folder", os.fspath(self.loc_default) + ) + if not dirname: + return + self.loc_default = Path(dirname).absolute() + self.update_project_location() + + def update_project_name(self, text): + self.proj_default = text + self.update_project_location() + + def update_experimenter_name(self, text): + self.exp_default = text + self.update_project_location() + + def update_project_location(self): + full_name = self.name_default.format(self.proj_default, self.exp_default) + full_path = self.loc_default / full_name + self.loc_line.setText(os.fspath(full_path)) diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py new file mode 100644 index 0000000000..cb8ad5e0a4 --- /dev/null +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -0,0 +1,805 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +import re +from importlib import import_module +from pathlib import Path + +import dlclibrary +from PySide6 import QtWidgets +from PySide6.QtCore import Qt, Slot + +import deeplabcut +import deeplabcut.compat as compat +from deeplabcut.core.engine import Engine +from deeplabcut.core.weight_init import WeightInitialization +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, + _create_confirmation_box, + _create_grid_layout, + _create_label_widget, + _create_message_box, + set_combo_items, +) +from deeplabcut.gui.displays.shuffle_metadata_viewer import ShuffleMetadataViewer +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.widgets import launch_napari +from deeplabcut.modelzoo import build_weight_init +from deeplabcut.pose_estimation_pytorch import ( + available_models, + is_model_cond_top_down, + is_model_top_down, +) +from deeplabcut.utils.auxiliaryfunctions import ( + get_data_and_metadata_filenames, + get_training_set_folder, +) + + +class CreateTrainingDataset(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self.model_comparison = False + + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_grid_layout(margins=(20, 0, 0, 0)) + self._generate_layout_attributes(self.layout_attributes) + self.main_layout.addLayout(self.layout_attributes) + + self.mapping_button = QtWidgets.QPushButton("Edit Conversion Table") + self.mapping_button.clicked.connect(self.edit_conversion_table) + self.mapping_button.setVisible(False) + self.root.engine_change.connect(self.set_edit_table_visibility) + + self.ok_button = QtWidgets.QPushButton("Create Training Dataset") + self.ok_button.setMinimumWidth(150) + self.ok_button.clicked.connect(self.create_training_dataset) + + self.main_layout.addWidget(self.mapping_button, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.ok_button, alignment=Qt.AlignRight) + + self.view_shuffles_button = QtWidgets.QPushButton("View Existing Shuffles") + self.view_shuffles_button.clicked.connect(self.view_shuffles) + self.main_layout.addWidget(self.view_shuffles_button, alignment=Qt.AlignLeft) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + 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")) + 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) + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + if self.root.is_multianimal: + func = deeplabcut.create_multianimaltraining_dataset + else: + func = deeplabcut.create_training_dataset + label = QtWidgets.QLabel(func.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_attributes(self, layout): + layout.setColumnMinimumWidth(3, 300) + + # Shuffle + shuffle_label = QtWidgets.QLabel("Shuffle") + self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + + # Dataset choices + self.weight_init_label = QtWidgets.QLabel("Weight Initialization") + self.weight_init_selector = WeightInitializationSelector(self.root) + self.update_weight_init_methods(self.root.engine) + self.root.engine_change.connect(self.update_weight_init_methods) + + # Augmentation method + augmentation_label = QtWidgets.QLabel("Augmentation method") + self.aug_choice = QtWidgets.QComboBox() + self.update_aug_methods(self.root.engine) + self.root.engine_change.connect(self.update_aug_methods) + self.aug_choice.currentTextChanged.connect(self.log_augmentation_choice) + + # Neural Network + nnet_label = QtWidgets.QLabel("Network architecture") + self.net_choice = QtWidgets.QComboBox() + self.net_choice.setMinimumWidth(200) + self.update_nets(self.root.engine) + self.root.engine_change.connect(self.update_nets) + 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.set_edit_table_visibility() + ) + + # Detector selection for top-down models + self.detector_label = QtWidgets.QLabel("Detector architecture") + 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.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.update_conditions(engine=self.root.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) + ) + + # Overwrite selection + self.overwrite = QtWidgets.QCheckBox("Overwrite if exists") + self.overwrite.setChecked(False) + self.overwrite.setToolTip( + "When checked, creating a new shuffle with an index that already exists " + "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}")) + + # Use same data split as another shuffle + self.data_split_selection = DataSplitSelector(self.root, self) + + layout.addWidget(shuffle_label, 0, 0) + layout.addWidget(self.shuffle, 0, 1) + layout.addWidget(self.weight_init_label, 0, 2) + layout.addWidget(self.weight_init_selector, 0, 3) + + layout.addWidget(nnet_label, 1, 0) + layout.addWidget(self.net_choice, 1, 1) + layout.addWidget(augmentation_label, 1, 2) + layout.addWidget(self.aug_choice, 1, 3) + + layout.addWidget(self.detector_label, 2, 0) + layout.addWidget(self.detector_choice, 2, 1) + + layout.addWidget(self.conditions_label, 3, 0) + layout.addWidget(self.conditions_selection_widget, 3, 1) + + layout.addWidget(self.overwrite, 4, 0) + layout.addWidget(self.data_split_selection, 5, 0) + + def log_net_choice(self, net): + self.root.logger.info(f"Network architecture set to {net.upper()}") + + def log_augmentation_choice(self, augmentation): + self.root.logger.info(f"Image augmentation set to {augmentation.upper()}") + + def edit_conversion_table(self): + # Test beforehand whether a conversion table exists + memory_replay_folder = Path(self.root.project_folder) / "memory_replay" + conversion_matrix_out_path = str(memory_replay_folder / "confusion_matrix.png") + files = [self.root.config_path] + if Path(conversion_matrix_out_path).exists(): + files.append(conversion_matrix_out_path) + _ = launch_napari(files) + + def create_training_dataset(self): + shuffle = self.shuffle.value() + cfg = self.root.cfg + existing_indices = get_existing_shuffle_indices( + cfg=cfg, train_fraction=cfg["TrainingFraction"][self.root.trainingset_index] + ) + + overwrite = self.overwrite.isChecked() + if shuffle in existing_indices: + if overwrite: + if not self._confirm_overwrite(shuffle, existing_indices): + return + else: + msg = _create_message_box( + "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 " + f"shuffles are {existing_indices}) or you can overwrite the " + f"shuffle by ticking the 'Overwrite' checkbox" + ), + ) + msg.exec_() + self.root.writer.write("Training dataset creation failed.") + return + + if self.model_comparison: + raise NotImplementedError + # TODO: finish model_comparison + # deeplabcut.create_training_model_comparison( + # config_file, + # num_shuffles=shuffle, + # net_types=self.net_type, + # augmenter_types=self.aug_type, + # ) + else: + try: + engine = self.root.engine + net_type = self.net_choice.currentText() + detector_type = None + ctd_conditions = None + if engine == Engine.TF: + import_module("tensorflow") + + # try importing TF so they can't create shuffles for it if they + # don't have it installed + elif engine == Engine.PYTORCH: + if is_model_top_down(net_type): + detector_type = self.detector_choice.currentText() + elif is_model_cond_top_down(net_type): + ctd_conditions = self._build_ctd_conditions( + self.conditions_selection_widget.selected_conditions + ) + + try: + 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}.") + return + + if self.data_split_selection.selected: + deeplabcut.create_training_dataset_from_existing_split( + self.root.config_path, + from_shuffle=self.data_split_selection.from_shuffle, + shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + + elif self.root.is_multianimal: + deeplabcut.create_multianimaltraining_dataset( + self.root.config_path, + shuffle, + Shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + else: + deeplabcut.create_training_dataset( + self.root.config_path, + shuffle, + Shuffles=[self.shuffle.value()], + net_type=net_type, + detector_type=detector_type, + augmenter_type=self.aug_choice.currentText(), + userfeedback=not overwrite, + weight_init=weight_init, + engine=engine, + ctd_conditions=ctd_conditions, + ) + except ValueError as err: + msg = _create_message_box( + "The training dataset could not be created.", + str(err), + ) + msg.exec_() + return + except ModuleNotFoundError as err: + info_text = ( + f"Error `{err}`. If the error is `ModuleNotFoundError: No module " + "named 'tensorflow'`, this is because you tried creating a " + "TensorFlow shuffle, but TensorFlow is not installed in your " + "environment. To create TensorFlow shuffles (and use TensorFlow " + "models), install it with\n" + " Windows/Linux:\n" + " pip install 'deeplabcut[tf]'\n" + " Apple Silicon:\n" + " pip install 'deeplabcut[apple_mchips]'" + ) + msg = _create_message_box("The training dataset could not be created.", info_text) + msg.exec_() + return + + # Check that training data files were indeed created. + trainingsetfolder = get_training_set_folder(self.root.cfg) + filenames = list( + get_data_and_metadata_filenames( + trainingsetfolder, + self.root.cfg["TrainingFraction"][0], + self.shuffle.value(), + self.root.cfg, + ) + ) + if self.root.is_multianimal: + filenames[0] = filenames[0].with_suffix(".pickle") + if all((Path(self.root.project_folder) / file).exists() for file in filenames): + self.root.shuffle_created.emit(self.shuffle.value()) + msg = _create_message_box( + "The training dataset is successfully created.", + "Use the function 'train_network' to start training. Happy training!", + ) + msg.exec_() + self.root.writer.write("Training dataset successfully created.") + else: + msg = _create_message_box( + "The training dataset could not be created.", + "Make sure there are annotated data under labeled-data.", + ) + msg.exec_() + 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. + + Args: + shuffle: the shuffle the user wants to overwrite + existing_indices: the indices of existing shuffles + + Returns: + whether the user confirmed overwriting the shuffle + """ + try: + engine = get_shuffle_engine(self.root.cfg, self.root.trainingset_index, shuffle) + engine_str = f" (with engine '{engine.aliases[0]}')" + except ValueError: + engine_str = "" + + conf = _create_confirmation_box( + title=f"Are you sure you want to overwrite shuffle {shuffle}?", + description=( + 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=(f"You can create a shuffle with another index. Existing indices are {existing_indices}"), + ) + msg.exec_() + self.root.writer.write("Training dataset creation interrupted.") + return False + + return True + + def _build_ctd_conditions(self, conditions_path: str | Path) -> Path | tuple[int, str]: + """ + Builds CTD conditions in appropriate format from path to conditions. + + Args: + conditions_path: str | Path: + path to conditions (path to snapshot or to predictions) + + Returns: + ctd_conditions: Path | tuple[int, str] + ctd conditions in the right format for deeplabcut.create_training_dataset() API method. + + Raises: + ValueError: If conditions are missing or invalid. + """ + if conditions_path is None: + raise ValueError("No conditions were selected for CTD model.") + else: + conditions_path = Path(conditions_path) + if conditions_path.suffix.lower() in [".h5", ".json"]: + return conditions_path + elif conditions_path.suffix.lower() == ".pt": + match = re.search(r"shuffle(\d+)", str(conditions_path)) + if match: + shuffle_number = int(match.group(1)) + else: + raise ValueError("Shuffle number could not be extracted from path.") + snapshot_filename = conditions_path.name + return shuffle_number, snapshot_filename + else: + raise ValueError("Unsupported conditions file type") + + @Slot(Engine) + def update_nets(self, engine: Engine | None) -> None: + if engine is None: + engine = self.root.engine + + default_net = None + if engine == Engine.TF: + nets = DLCParams.NNETS.copy() + if not self.root.is_multianimal: + nets.remove("dlcrnet_ms5") + else: + nets = available_models() + net_filter = self.get_net_filter() + default_net = self.get_default_net() + td_prefix = "top_down_" + if net_filter is not 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 default_net is None: + default_net = self.root.cfg.get("default_net_type", "resnet_50") + if ( + engine == Engine.TF + and default_net not in DLCParams.NNETS + or engine == Engine.PYTORCH + and default_net not in available_models() + ): + default_net = "resnet_50" + + set_combo_items( + combo_box=self.net_choice, + items=nets, + index=nets.index(default_net) if default_net in nets else 0, + ) + + @Slot(Engine) + def update_detectors( + self, + engine: Engine | None = None, + net_choice: str | None = None, + ) -> None: + if engine is None: + engine = self.root.engine + + if engine == Engine.TF: + detectors = [] + else: + # FIXME: Circular imports make it impossible to import this at the top + from deeplabcut.pose_estimation_pytorch import available_detectors + + detectors = available_detectors() + det_filter = self.get_detector_filter() + if det_filter is not None: + detectors = [d for d in detectors if d in det_filter] + + default_detector = self.get_default_detector() + try: + index = detectors.index(default_detector) + except ValueError: + try: + index = detectors.index("ssdlite") + except ValueError: + index = -1 + set_combo_items( + combo_box=self.detector_choice, + items=detectors, + index=index, + ) + + if net_choice is None: + net_choice = self.net_choice.currentText() + + if engine == Engine.PYTORCH and is_model_top_down(net_choice): + self.detector_label.show() + self.detector_choice.show() + else: + self.detector_label.hide() + self.detector_choice.hide() + + @Slot(Engine) + def update_conditions( + self, + engine: Engine | None = None, + net_choice: str | None = None, + ) -> None: + if engine is None: + engine = self.root.engine + + if net_choice is None: + net_choice = self.net_choice.currentText() + + if engine == Engine.PYTORCH and is_model_cond_top_down(net_choice): + self.conditions_label.show() + self.conditions_selection_widget.show() + else: + self.conditions_label.hide() + self.conditions_selection_widget.hide() + + @Slot(Engine) + def update_aug_methods(self, engine: Engine) -> None: + methods = compat.get_available_aug_methods(engine) + set_combo_items( + combo_box=self.aug_choice, + items=methods, + index=0, + ) + + @Slot(Engine) + def update_weight_init_methods(self, engine: Engine) -> None: + if engine != Engine.PYTORCH: + self.weight_init_label.hide() + self.weight_init_selector.hide() + return + + self.weight_init_label.show() + self.weight_init_selector.update_choices(list(_WEIGHT_INIT_OPTIONS.keys())) + self.weight_init_selector.show() + + def get_net_filter(self) -> list[str] | None: + """Returns: the net type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + if "super_animal" in weight_init_cfg: + return dlclibrary.get_available_models(weight_init_cfg["super_animal"]) + + return None + + def get_detector_filter(self) -> list[str] | None: + """Returns: the detectors that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + if "super_animal" in weight_init_cfg: + return dlclibrary.get_available_detectors(weight_init_cfg["super_animal"]) + + return None + + def get_default_net(self) -> str | None: + """Returns: the net type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + return weight_init_cfg.get("default_net") + + def get_default_detector(self) -> str | None: + """Returns: the detector type that can be used based on weight initialization""" + if self.root.engine != Engine.PYTORCH: + return None + + if self.weight_init_selector.weight_init not in _WEIGHT_INIT_OPTIONS: + return None + + weight_init_cfg = _WEIGHT_INIT_OPTIONS[self.weight_init_selector.weight_init] + return weight_init_cfg.get("default_detector") + + def view_shuffles(self) -> None: + viewer = ShuffleMetadataViewer(root=self.root, parent=self) + viewer.show() + + +class WeightInitializationSelector(QtWidgets.QWidget): + """Widget to select weight initialization.""" + + def __init__(self, root): + super().__init__() + self.root = root + + self.weight_init_choice = QtWidgets.QComboBox() + + self.memory_replay_label = QtWidgets.QLabel("With memory replay") + self.memory_replay_box = QtWidgets.QCheckBox() + self.memory_replay_label.hide() + self.memory_replay_box.hide() + + memory_replay_layout = QtWidgets.QHBoxLayout() + memory_replay_layout.addWidget(self.memory_replay_label) + memory_replay_layout.addWidget(self.memory_replay_box) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.weight_init_choice) + layout.addLayout(memory_replay_layout) + self.setLayout(layout) + + self.weight_init_choice.currentTextChanged.connect(self._choice_changed) + + @property + def weight_init(self) -> str: + return self.weight_init_choice.currentText() + + @property + def with_decoder(self) -> bool: + weight_init_choice = self.weight_init_choice.currentText() + return "fine-tuning" in weight_init_choice.lower() + + @property + 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.""" + set_combo_items( + combo_box=self.weight_init_choice, + items=choices, + ) + + def get_super_animal_weight_init( + self, + net_type: str, + detector_type: str, + ) -> WeightInitialization | None: + """ + Args: + net_type: The architecture of the pose model from which to fine-tune a + SuperAnimal model. + detector_type: The architecture of the detector from which to fine-tune a + SuperAnimal model. + + Raises: + ValueError: If WeightInitialization should be defined but could not be + created (e.g. if there's no conversion table). + """ + if self.root.engine != Engine.PYTORCH: + return None + + weight_init_choice = self.weight_init_choice.currentText() + if "imagenet" in weight_init_choice.lower(): + return + + weight_init_data = _WEIGHT_INIT_OPTIONS[weight_init_choice] + super_animal = weight_init_data["super_animal"] + if net_type.startswith("top_down_"): + net_type = net_type[len("top_down_") :] + try: + weight_init = build_weight_init( + self.root.cfg, + super_animal=super_animal, + model_name=net_type, + detector_name=detector_type, + with_decoder=self.with_decoder, + memory_replay=self.memory_replay, + ) + except ValueError as err: + QtWidgets.QMessageBox.critical( + self, + "Error", + ( + f"No Conversion table specified for {super_animal} in the project " + "configuration file. Please create a conversion table using the GUI" + ", with ``deeplabcut.modelzoo.utils.create_conversion_table``, or " + "by adding it to your project's configuration file manually." + ), + ) + raise err + + return weight_init + + def _choice_changed(self, state: str) -> None: + if "fine-tuning" in str(state).lower(): + self.memory_replay_label.show() + self.memory_replay_box.show() + else: + self.memory_replay_label.hide() + self.memory_replay_box.hide() + + +class DataSplitSelector(QtWidgets.QWidget): + """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__() + self.root = root + self.parent = parent + + self.setToolTip( + "This allows you to create a shuffle where the data split is the same as " + "one of your existing shuffles (the images on which the model is " + "trained/tested are the same)." + ) + + layout = QtWidgets.QVBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + box_layout = QtWidgets.QHBoxLayout() + box_layout.setSpacing(0) + box_layout.setContentsMargins(0, 0, 0, 0) + + selector_layout = QtWidgets.QHBoxLayout() + selector_layout.setSpacing(0) + selector_layout.setContentsMargins(0, 0, 0, 0) + + self.shuffle_label = QtWidgets.QLabel("From shuffle:") + self.shuffle_label.hide() + self.shuffle_selector = QtWidgets.QSpinBox() + self.shuffle_selector.setMaximum(10_000) + self.shuffle_selector.setValue(0) + self.shuffle_selector.hide() + + self.box = QtWidgets.QCheckBox(parent=self) + self.box.stateChanged.connect(self._checkbox_status_changed) + self.box_label = QtWidgets.QLabel("Use an existing data split") + + box_layout.addWidget(self.box) + box_layout.addWidget(self.box_label) + selector_layout.addWidget(self.shuffle_label) + selector_layout.addWidget(self.shuffle_selector) + layout.addLayout(box_layout) + layout.addLayout(selector_layout) + self.setLayout(layout) + + @property + def selected(self) -> bool: + return self.box.isChecked() + + @property + def from_shuffle(self) -> int: + """The shuffle from which to copy the data split.""" + return self.shuffle_selector.value() + + def _checkbox_status_changed(self, state: int) -> None: + if Qt.CheckState(state) == Qt.Checked: + self.shuffle_selector.show() + self.shuffle_label.show() + else: + self.shuffle_selector.hide() + self.shuffle_label.hide() + + +_WEIGHT_INIT_OPTIONS = { # FIXME - Generate dynamically + "Transfer Learning - ImageNet": { + "model_filter": None, + "detector_filter": None, + }, + "Transfer Learning - SuperAnimal Bird": { + "default_net": "top_down_resnet_50", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_bird", + }, + "Transfer Learning - SuperAnimal Quadruped": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_quadruped", + }, + "Transfer Learning - SuperAnimal TopViewMouse": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_topviewmouse", + }, + "Fine-tuning - SuperAnimal Bird": { + "default_net": "top_down_resnet_50", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_bird", + }, + "Fine-tuning - SuperAnimal Quadruped": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_quadruped", + }, + "Fine-tuning - SuperAnimal TopViewMouse": { + "default_net": "top_down_hrnet_w32", + "default_detector": "fasterrcnn_mobilenet_v3_large_fpn", + "super_animal": "superanimal_topviewmouse", + }, +} diff --git a/deeplabcut/gui/tabs/create_videos.py b/deeplabcut/gui/tabs/create_videos.py new file mode 100644 index 0000000000..638c616d6f --- /dev/null +++ b/deeplabcut/gui/tabs/create_videos.py @@ -0,0 +1,301 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +import deeplabcut +from deeplabcut.gui.components import ( + BodypartListWidget, + DefaultTab, + ShuffleSpinBox, + VideoSelectionWidget, + _create_horizontal_layout, + _create_label_widget, + _create_vertical_layout, +) + + +class CreateVideos(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self.skeleton_builder = None + + self.bodyparts_to_use = self.root.all_bodyparts + self._set_page() + + @property + def files(self): + return self.video_selection_widget.files + + def _set_page(self): + self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) + self.video_selection_widget = VideoSelectionWidget(self.root, self) + self.main_layout.addWidget(self.video_selection_widget) + + tmp_layout = _create_horizontal_layout() + + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_horizontal_layout(margins=(0, 0, 0, 0)) + self._generate_layout_attributes(self.layout_attributes) + tmp_layout.addLayout(self.layout_attributes) + + self.layout_multianimal = _create_horizontal_layout() + + if self.root.is_multianimal: + self._generate_layout_multianimal(self.layout_multianimal) + tmp_layout.addLayout(self.layout_multianimal) + + self.main_layout.addLayout(tmp_layout) + + 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) + + self.sk_button = QtWidgets.QPushButton("Build skeleton") + self.sk_button.clicked.connect(self.build_skeleton) + self.main_layout.addWidget(self.sk_button, alignment=Qt.AlignRight) + + self.run_button = QtWidgets.QPushButton("Create videos") + self.run_button.clicked.connect(self.create_videos) + self.main_layout.addWidget(self.run_button, alignment=Qt.AlignRight) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + + def _on_skeleton_builder_destroyed(self): + self.skeleton_builder = None + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel(deeplabcut.create_labeled_video.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_multianimal(self, layout): + tmp_text = QtWidgets.QLabel("Color keypoints by:") + self.color_by_widget = QtWidgets.QComboBox() + self.color_by_widget.addItems(["bodypart", "individual"]) + self.color_by_widget.setCurrentText("bodypart") + self.color_by_widget.currentTextChanged.connect(self.update_color_by) + + layout.addWidget(tmp_text) + layout.addWidget(self.color_by_widget) + + def _generate_layout_attributes(self, layout): + # Shuffle + opt_text = QtWidgets.QLabel("Shuffle") + self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + + layout.addWidget(opt_text) + layout.addWidget(self.shuffle) + + # Overwrite videos + self.overwrite_videos = QtWidgets.QCheckBox("Overwrite videos") + self.overwrite_videos.setCheckState(Qt.Unchecked) + self.overwrite_videos.stateChanged.connect(self.update_overwrite_videos) + + layout.addWidget(self.overwrite_videos) + + def _generate_layout_video_parameters(self, layout): + tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) + + # Trail Points + opt_text = QtWidgets.QLabel("Specify the number of trail points") + self.trail_points = QtWidgets.QSpinBox() + self.trail_points.setValue(0) + tmp_layout.addWidget(opt_text) + tmp_layout.addWidget(self.trail_points) + + layout.addLayout(tmp_layout) + + tmp_layout = _create_vertical_layout(margins=(0, 0, 0, 0)) + + # Plot all bodyparts + self.plot_all_bodyparts = QtWidgets.QCheckBox("Plot all bodyparts") + self.plot_all_bodyparts.setCheckState(Qt.Checked) + self.plot_all_bodyparts.stateChanged.connect(self.update_use_all_bodyparts) + tmp_layout.addWidget(self.plot_all_bodyparts) + + # Skeleton + self.draw_skeleton_checkbox = QtWidgets.QCheckBox("Draw skeleton") + self.draw_skeleton_checkbox.setCheckState(Qt.Unchecked) + self.draw_skeleton_checkbox.stateChanged.connect(self.update_draw_skeleton) + tmp_layout.addWidget(self.draw_skeleton_checkbox) + + # 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) + tmp_layout.addWidget(self.use_filtered_data_checkbox) + + # Selector for p-cutoff + pcutoff_widget = QtWidgets.QWidget() + pcutoff_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) + pcutoff_label = QtWidgets.QLabel("Plotting confidence cutoff (pcutoff)") + self.pcutoff_selector = QtWidgets.QDoubleSpinBox() + self.pcutoff_selector.setMinimum(0.0) + self.pcutoff_selector.setMaximum(1.0) + self.pcutoff_selector.setValue(0.6) + self.pcutoff_selector.setSingleStep(0.05) + pcutoff_layout.addWidget(pcutoff_label) + 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." + ) + tmp_layout.addWidget(pcutoff_widget) + + # Plot trajectories + self.plot_trajectories = QtWidgets.QCheckBox("Plot trajectories") + self.plot_trajectories.setCheckState(Qt.Unchecked) + self.plot_trajectories.stateChanged.connect(self.update_plot_trajectory_choice) + 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.setCheckState(Qt.Unchecked) + 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)) + nested_tmp_layout.addLayout(tmp_layout) + + tmp_layout = _create_vertical_layout(margins=(0, 0, 0, 0)) + + # Bodypart list + self.bodyparts_list_widget = BodypartListWidget( + root=self.root, + parent=self, + ) + nested_tmp_layout.addWidget(self.bodyparts_list_widget, Qt.AlignLeft) + + tmp_layout.addLayout(nested_tmp_layout, Qt.AlignLeft) + + layout.addLayout(tmp_layout, Qt.AlignLeft) + + def update_high_quality_video(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"High quality {s}.") + + def update_plot_trajectory_choice(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + 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}") + self.bodyparts_to_use = selected_bodyparts + + def update_use_all_bodyparts(self, s): + if Qt.CheckState(s) == Qt.Checked: + self.bodyparts_list_widget.setEnabled(False) + self.bodyparts_list_widget.hide() + self.root.logger.info("Plot all bodyparts ENABLED.") + + else: + self.bodyparts_list_widget.setEnabled(True) + self.bodyparts_list_widget.show() + self.root.logger.info("Plot all bodyparts DISABLED.") + + def update_use_filtered_data(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Use filtered data {s}") + + def update_draw_skeleton(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Draw skeleton {s}") + + def update_overwrite_videos(self, state): + s = "ENABLED" if Qt.CheckState(state) == Qt.Checked else "DISABLED" + self.root.logger.info(f"Overwrite videos {s}") + + def update_color_by(self, text): + self.root.logger.info(f"Coloring keypoints in videos by {text}") + + def update_filter_choice(self, rb): + self.filtered = rb.text() == "Yes" + + def update_video_slow_choice(self, rb): + self.slow = rb.text() == "Yes" + + def update_draw_skeleton_choice(self, rb): + self.draw = rb.text() == "Yes" + + def create_videos(self): + config_path = self.root.config_path + shuffle = self.root.shuffle_value + videos = self.files + trailpoints = self.trail_points.value() + if hasattr(self, "color_by_widget"): + # Multianimal scenario. + # Color is based on individual or bodypart. + color_by = self.color_by_widget.currentText() + else: + # Single animal scenario. + # Color is based on bodypart. + color_by = "bodypart" + filtered = self.use_filtered_data_checkbox.isChecked() + + bodyparts = "all" + if len(self.bodyparts_to_use) != 0 and not self.plot_all_bodyparts.isChecked(): + self.update_selected_bodyparts() + bodyparts = self.bodyparts_to_use + + videos_created = deeplabcut.create_labeled_video( + config=config_path, + videos=videos, + shuffle=shuffle, + filtered=filtered, + save_frames=self.create_high_quality_video.isChecked(), + pcutoff=self.pcutoff_selector.value(), + displayedbodyparts=bodyparts, + draw_skeleton=self.draw_skeleton_checkbox.isChecked(), + trailpoints=trailpoints, + color_by=color_by, + overwrite=self.overwrite_videos.isChecked(), + ) + if all(videos_created): + self.root.writer.write("Labeled videos created.") + else: + 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}.") + + if self.plot_trajectories.isChecked(): + deeplabcut.plot_trajectories( + config=config_path, + videos=videos, + shuffle=shuffle, + filtered=filtered, + displayedbodyparts=bodyparts, + pcutoff=self.pcutoff_selector.value(), + ) + + def build_skeleton(self, *args): + from deeplabcut.gui.widgets import SkeletonBuilder + + if self.skeleton_builder is None: + self.skeleton_builder = SkeletonBuilder( + config_path=self.root.config_path, + parent=self.root, + ) + self.skeleton_builder.destroyed.connect(self._on_skeleton_builder_destroyed) + self.skeleton_builder.show() diff --git a/deeplabcut/gui/tabs/docs.py b/deeplabcut/gui/tabs/docs.py new file mode 100644 index 0000000000..1f52118e19 --- /dev/null +++ b/deeplabcut/gui/tabs/docs.py @@ -0,0 +1,15 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +BASE_URL = "https://deeplabcut.github.io/DeepLabCut/docs/" +README = "https://deeplabcut.github.io/DeepLabCut/README.html" +URL_3D = BASE_URL + "Overviewof3D.html" +URL_MA_CONFIGURE = BASE_URL + "maDLC_UserGuide.html#configure-the-project" +URL_USE_GUIDE_SCENARIO = BASE_URL + "UseOverviewGuide.html#what-scenario-do-you-have" diff --git a/deeplabcut/gui/tabs/evaluate_network.py b/deeplabcut/gui/tabs/evaluate_network.py new file mode 100644 index 0000000000..bc39df9e7e --- /dev/null +++ b/deeplabcut/gui/tabs/evaluate_network.py @@ -0,0 +1,229 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +from pathlib import Path + +import matplotlib.image as mpimg +from matplotlib.backends.backend_qt5agg import ( + FigureCanvasQTAgg as FigureCanvas, +) +from matplotlib.figure import Figure +from PySide6 import QtWidgets +from PySide6.QtCore import Qt, Slot + +import deeplabcut +from deeplabcut.core.engine import Engine +from deeplabcut.gui.components import ( + BodypartListWidget, + DefaultTab, + ShuffleSpinBox, + _create_horizontal_layout, + _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 + + +class GridCanvas(QtWidgets.QDialog): + def __init__(self, image_paths, parent=None): + super().__init__(parent) + self.image_paths = image_paths + layout = QtWidgets.QVBoxLayout(self) + self.figure = Figure() + self.figure.patch.set_facecolor("None") + self.grid = self.figure.add_gridspec(3, 3) + self.canvas = FigureCanvas(self.figure) + layout.addWidget(self.canvas) + + 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) + ax.imshow(img) + + +class EvaluateNetwork(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self.bodyparts_to_use = self.root.all_bodyparts + + self._set_page() + + def _set_page(self): + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_horizontal_layout() + self._generate_layout_attributes(self.layout_attributes) + self.main_layout.addLayout(self.layout_attributes) + + self.main_layout.addWidget(_create_label_widget("")) # dummy text + self.layout_additional_attributes = _create_vertical_layout() + self._generate_additional_attributes(self.layout_additional_attributes) + self.main_layout.addLayout(self.layout_additional_attributes) + + self.ev_nw_button = QtWidgets.QPushButton("Evaluate Network") + self.ev_nw_button.setMinimumWidth(150) + self.ev_nw_button.clicked.connect(self.evaluate_network) + + self.opt_button = QtWidgets.QPushButton("Plot 3 test maps") + self.opt_button.setMinimumWidth(150) + self.opt_button.clicked.connect(self.plot_maps) + + self.edit_inferencecfg_btn = QtWidgets.QPushButton("Edit inference_cfg.yaml") + self.edit_inferencecfg_btn.setMinimumWidth(150) + 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.ev_nw_button, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.opt_button, alignment=Qt.AlignRight) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + + self.root.engine_change.connect(self._on_engine_change) + self._on_engine_change(self.root.engine) + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel(deeplabcut.evaluate_network.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_attributes(self, layout): + opt_text = QtWidgets.QLabel("Shuffle") + self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + self.shuffle_display = SelectedShuffleDisplay(self.root, row_margin=0) + + layout.addWidget(opt_text) + layout.addWidget(self.shuffle) + layout.addWidget(self.shuffle_display) + + def open_inferencecfg_editor(self): + editor = ConfigEditor(self.root.inference_cfg_path) + editor.show() + + def plot_maps(self): + shuffle = self.root.shuffle_value + config_path = self.root.config_path + deeplabcut.extract_save_all_maps(config_path, shuffle=shuffle, Indices=[0, 1, 2]) + + # Display all images + dest_folder = ( + Path(self.root.project_folder) + / str( + auxiliaryfunctions.get_evaluation_folder(self.root.cfg["TrainingFraction"][0], shuffle, self.root.cfg) + ) + / "maps" + ) + image_paths = [str(p) for p in Path(dest_folder).iterdir() if p.name.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.stateChanged.connect(self.update_plot_predictions) + + tmp_layout.addWidget(self.plot_predictions) + + self.bodyparts_list_widget = BodypartListWidget(root=self.root, parent=self) + self.use_all_bodyparts = QtWidgets.QCheckBox("Compare all bodyparts") + self.use_all_bodyparts.stateChanged.connect(self.update_bodypart_choice) + self.use_all_bodyparts.setCheckState(Qt.Checked) + + tmp_layout.addWidget(self.use_all_bodyparts) + layout.addLayout(tmp_layout) + + layout.addWidget(self.bodyparts_list_widget, alignment=Qt.AlignLeft) + + def update_map_choice(self, state): + if Qt.CheckState(state) == Qt.Checked: + self.root.logger.info("Plot scoremaps ENABLED") + else: + self.root.logger.info("Plot predictions DISABLED") + + def update_plot_predictions(self, s): + if Qt.CheckState(s) == Qt.Checked: + self.root.logger.info("Plot predictions ENABLED") + else: + self.root.logger.info("Plot predictions DISABLED") + + def update_bodypart_choice(self, s): + if Qt.CheckState(s) == Qt.Checked: + self.bodyparts_list_widget.setEnabled(False) + self.bodyparts_list_widget.hide() + self.root.logger.info("Use all bodyparts") + 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}") + + def evaluate_network(self): + try: + config_path = self.root.config_path + shuffle = self.root.shuffle_value + plotting = self.plot_predictions.isChecked() + + bodyparts_to_use = "all" + if ( + 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 + + deeplabcut.evaluate_network( + config_path, + Shuffles=[shuffle], + plotting=plotting, + show_errors=True, + comparisonbodyparts=bodyparts_to_use, + ) + + if plotting: + project_cfg = self.root.cfg + eval_folder = auxiliaryfunctions.get_evaluation_folder( + trainFraction=project_cfg["TrainingFraction"][0], + shuffle=shuffle, + cfg=project_cfg, + ) + scorer, _ = auxiliaryfunctions.get_scorer_name( + cfg=project_cfg, + shuffle=shuffle, + trainFraction=project_cfg["TrainingFraction"][0], + ) + + 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(labeled_images) + except Exception as error: + self.root.show_task_error(error, self.root.pose_cfg_path) + + @Slot(Engine) + def _on_engine_change(self, engine: Engine) -> None: + if engine == Engine.PYTORCH: + self.opt_button.hide() + return + + self.opt_button.show() diff --git a/deeplabcut/gui/tabs/extract_frames.py b/deeplabcut/gui/tabs/extract_frames.py new file mode 100644 index 0000000000..6c04c7f43e --- /dev/null +++ b/deeplabcut/gui/tabs/extract_frames.py @@ -0,0 +1,302 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from functools import partial +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +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 + + +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. + + Args: + config (string): Full path of the config.yaml file as a string. + videos (optional): List of videos whose cropping areas are to be defined. Note + that full paths are required. By default, all videos in the config are + successively loaded. Defaults to None. + + Returns: + dict: Updated project configuration. + """ + from deeplabcut.gui.widgets import FrameCropper + from deeplabcut.utils import auxiliaryfunctions + + cfg = auxiliaryfunctions.read_config(config) + if videos is None: + videos = list(cfg.get("video_sets_original") or cfg["video_sets"]) + + for video in videos: + fc = FrameCropper(video) + coords = fc.draw_bbox() + if coords: + temp = { + "crop": ", ".join( + map( + str, + [ + int(coords[0]), + int(coords[2]), + int(coords[1]), + int(coords[3]), + ], + ) + ) + } + try: + cfg["video_sets"][video] = temp + except KeyError: + cfg["video_sets_original"][video] = temp + + auxiliaryfunctions.write_config(config, cfg) + return cfg + + +class ExtractFrames(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + self.worker = None + self.thread = None + self._set_page() + + def _set_page(self): + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_grid_layout(margins=(0, 0, 0, 0)) + self._generate_layout_attributes(self.layout_attributes) + self.main_layout.addLayout(self.layout_attributes) + + self.main_layout.addWidget( + _create_label_widget( + "Frame extraction from a video subset (optional for automatic extraction)", + "font:bold", + ) + ) + self.video_selection_widget = VideoSelectionWidget(self.root, self) + self.main_layout.addWidget(self.video_selection_widget) + + self.ok_button = QtWidgets.QPushButton("Extract Frames") + self.ok_button.clicked.connect(self.extract_frames) + self.main_layout.addWidget(self.ok_button, alignment=Qt.AlignRight) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel(extract_frames.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_attributes(self, layout): + layout.setColumnMinimumWidth(1, 300) + # Extraction method + ext_method_label = QtWidgets.QLabel("Extraction method") + 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) + + # 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) + + # 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) + + # Cluster step + cluster_step_label = QtWidgets.QLabel("Cluster step") + self.cluster_step_widget = QtWidgets.QSpinBox() + self.cluster_step_widget.setValue(1) + + # GUI Slider width + gui_slider_label = QtWidgets.QLabel("GUI slider width") + self.slider_width_widget = QtWidgets.QSpinBox() + self.slider_width_widget.setValue(25) + self.slider_width_widget.setEnabled(False) + + layout.addWidget(ext_method_label, 1, 0) + layout.addWidget(self.extraction_method_widget, 1, 1) + layout.addWidget(gui_slider_label, 1, 2) + layout.addWidget(self.slider_width_widget, 1, 3) + + layout.addWidget(ext_algo_label, 2, 0) + layout.addWidget(self.extraction_algorithm_widget, 2, 1) + layout.addWidget(cluster_step_label, 2, 2) + layout.addWidget(self.cluster_step_widget, 2, 3) + + layout.addWidget(frame_crop_label, 3, 0) + layout.addWidget(self.frame_cropping_widget, 3, 1) + + def log_extraction_algorithm(self, extraction_algorithm): + self.root.logger.info(f"Extraction method set to {extraction_algorithm}") + + def log_extraction_method(self, extraction_method): + self.root.logger.info(f"Extraction method set to {extraction_method}") + if extraction_method == "manual": + self.extraction_algorithm_widget.setEnabled(False) + self.cluster_step_widget.setEnabled(False) + self.frame_cropping_widget.setEnabled(False) + self.slider_width_widget.setEnabled(True) + else: + self.extraction_algorithm_widget.setEnabled(True) + self.cluster_step_widget.setEnabled(True) + self.frame_cropping_widget.setEnabled(True) + self.slider_width_widget.setEnabled(False) + + def log_frame_cropping_choice(self, cropping_option): + self.root.logger.info(f"Cropping set to '{cropping_option}'") + + def extract_frames(self): + config_path = self.root.config_path + mode = self.extraction_method_widget.currentText() + if mode == "manual": + videos = list(self.video_selection_widget.files) + if not videos: + QtWidgets.QMessageBox.critical( + self, + "Error", + "Please select exactly one video to extract frames from.", + ) + return + first_video = videos[0] + if len(videos) > 1: + 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 + + algo = self.extraction_algorithm_widget.currentText() + clusterstep = self.cluster_step_widget.value() + slider_width = self.slider_width_widget.value() + + crop = False # default value + if self.frame_cropping_widget.currentText() == "GUI": + _ = select_cropping_area(config_path) + crop = True + elif self.frame_cropping_widget.currentText() == "read from config": + crop = True + + func = partial( + extract_frames, + config_path, + mode, + algo, + crop=crop, + cluster_step=clusterstep, + cluster_resizewidth=30, + cluster_color=False, + slider_width=slider_width, + userfeedback=False, + videos_list=self.video_selection_widget.files or None, + ) + + self.worker, self.thread = move_to_separate_thread(func, capture_outputs=True) + self._extract_error = False + self.worker.error.connect(self.root.show_task_error) + self.worker.error.connect(lambda _err: setattr(self, "_extract_error", True)) + self.worker.finished.connect(lambda: self.ok_button.setEnabled(True)) + self.worker.finished.connect(lambda: self.root._progress_bar.hide()) + self.thread.finished.connect(self._show_success_message) + self.thread.start() + self.ok_button.setEnabled(False) + self.root._progress_bar.show() + + def _show_success_message(self): + if getattr(self, "_extract_error", False): + return + + message = "Failed to create worker: it is None" + root_message = "failed to extract frames: worker is None" + if self.worker is not None: + failed = self.worker.outputs + if failed is None: + # outputs are None during manual frame extraction + return + + if len(failed) == 0: + 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." + root_message = "successfully extracted frames" + + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.setText(message) + msg.setWindowTitle("Info") + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + msg.exec_() + 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. + + 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 + true path of the video (which leads napari-deeplabcut to save the frames in the + incorrect folder). + + Args: + video_path: the path to a video in a DeepLabCut project or a video that was + added to the project + + Returns: + the path to the video (or symlink) in the project's 'videos' folder + + Raises: + FileNotFoundError: If there is no symlink or video in the 'videos' folder for + the given video + """ + video_path = Path(video_path).absolute() + project_videos = (Path(self.root.config_path).parent / "videos").absolute() + if video_path.parent == project_videos: + return video_path + + symlink_path = project_videos / video_path.name + if not symlink_path.exists(): + raise FileNotFoundError( + f"Could not find the video {video_path.name} in your project videos. " + f"Did you add the video (you can do so in the 'Manage Project' tab)? " + f"There should be a file in {symlink_path}." + ) + + return symlink_path diff --git a/deeplabcut/gui/tabs/extract_outlier_frames.py b/deeplabcut/gui/tabs/extract_outlier_frames.py new file mode 100644 index 0000000000..74774a2a5d --- /dev/null +++ b/deeplabcut/gui/tabs/extract_outlier_frames.py @@ -0,0 +1,169 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +import deeplabcut +from deeplabcut.gui.components import ( + DefaultTab, + ShuffleSpinBox, + VideoSelectionWidget, + _create_horizontal_layout, + _create_label_widget, +) +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.widgets import launch_napari + + +class ExtractOutlierFrames(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + self.filelist = [] + + self._set_page() + + @property + def files(self): + return self.video_selection_widget.files + + def _set_page(self): + self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) + self.video_selection_widget = VideoSelectionWidget(self.root, self) + self.main_layout.addWidget(self.video_selection_widget) + + self.main_layout.addWidget(_create_label_widget("Attributes", "font:bold")) + self.layout_attributes = _create_horizontal_layout() + self._generate_layout_attributes(self.layout_attributes) + + 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.layout_extraction_options = _create_horizontal_layout() + self._generate_layout_extraction_options(self.layout_extraction_options) + self.main_layout.addLayout(self.layout_extraction_options) + + self.extract_outlierframes_button = QtWidgets.QPushButton("Extract frames") + self.extract_outlierframes_button.clicked.connect(self.extract_outlier_frames) + self.extract_outlierframes_button.setMinimumWidth(150) + + self.label_outliers_button = QtWidgets.QPushButton("Labeling GUI") + self.label_outliers_button.setEnabled(True) + self.label_outliers_button.clicked.connect(self.launch_refinement_gui) + self.label_outliers_button.setMinimumWidth(150) + + self.merge_data_button = QtWidgets.QPushButton("Merge data") + 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.label_outliers_button, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.merge_data_button, alignment=Qt.AlignRight) + + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) + + def show_help_dialog(self): + dialog = QtWidgets.QDialog(self) + layout = QtWidgets.QVBoxLayout() + label = QtWidgets.QLabel(deeplabcut.extract_outlier_frames.__doc__, self) + scroll = QtWidgets.QScrollArea() + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setWidgetResizable(True) + scroll.setWidget(label) + layout.addWidget(scroll) + dialog.setLayout(layout) + dialog.exec_() + + def _generate_layout_attributes(self, layout): + # Shuffle + opt_text = QtWidgets.QLabel("Shuffle") + self.shuffle = ShuffleSpinBox(root=self.root, parent=self) + + layout.addWidget(opt_text) + layout.addWidget(self.shuffle) + + def _generate_multianimal_options(self, layout): + opt_text = QtWidgets.QLabel("Tracking method") + self.tracker_type_widget = QtWidgets.QComboBox() + self.tracker_type_widget.addItems(DLCParams.TRACKERS) + self.tracker_type_widget.currentTextChanged.connect(self.update_tracker_type) + + layout.addWidget(opt_text) + layout.addWidget(self.tracker_type_widget) + if not self.root.is_multianimal: + opt_text.hide() + self.tracker_type_widget.hide() + + def _generate_layout_extraction_options(self, layout): + opt_text = QtWidgets.QLabel("Specify the algorithm") + 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) + + layout.addWidget(opt_text) + layout.addWidget(self.outlier_algorithm_widget) + + 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") + + def extract_outlier_frames(self): + config_path = self.root.config_path + shuffle = self.root.shuffle_value + videos = self.files + videotype = self.video_selection_widget.videotype_widget.currentText() + outlieralgorithm = self.outlier_algorithm_widget.currentText() + track_method = "" + if self.root.is_multianimal: + track_method = self.tracker_type_widget.currentText() + + self.root.logger.debug( + f"""Running extract outlier frames with options: + config_path: {config_path}, + shuffle: {shuffle}, + videos: {videos}, + video_extensions: {videotype}, + outlier algorithm: {outlieralgorithm}, + track method: {track_method} + """ + ) + deeplabcut.extract_outlier_frames( + config=config_path, + videos=videos, + video_extensions=videotype, + shuffle=shuffle, + outlieralgorithm=outlieralgorithm, + track_method=track_method, + automatic=True, + ) + + def launch_refinement_gui(self): + self.merge_data_button.setEnabled(True) + _ = launch_napari() + + 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?" + ) + msg.setWindowTitle("Warning") + msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + result = msg.exec_() + if result == QtWidgets.QMessageBox.Yes: + deeplabcut.merge_datasets(self.root.config_path, forceiterate=None) diff --git a/deeplabcut/gui/tabs/label_frames.py b/deeplabcut/gui/tabs/label_frames.py new file mode 100644 index 0000000000..2b4a92550d --- /dev/null +++ b/deeplabcut/gui/tabs/label_frames.py @@ -0,0 +1,152 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import Qt + +from deeplabcut.generate_training_dataset import check_labels +from deeplabcut.gui.components import DefaultTab +from deeplabcut.gui.widgets import SkeletonBuilder, launch_napari + + +def label_frames(config_path: str | Path | None = None, image_folder: str | Path | None = None): + """Launches the napari-deeplabcut labelling GUI. + + For more information on labelling data with napari-deeplabcut, see our docs: + https://github.com/DeepLabCut/napari-deeplabcut?tab=readme-ov-file#usage + + If no parameters are given, the napari-deeplabcut labelling GUI is simply open, + and the folder containing the images to label can be dropped into the GUI. + + If the `config_path` and the `image_folder` are given as arguments, the given + `image_folder` for the project is opened in the napari-deeplabcut GUI to be labeled. + If only the `config_path` is given, the first image folder is opened. + + Args: + config_path (str | Path | None, optional): Full path of the project config.yaml + file. Defaults to None. + image_folder (str | Path | None, optional): Name of the image folder to open for + labelling. + Defaults to None. + + Examples: + Opening the napari-deeplabcut annotation GUI without opening a specific folder of + images to label. You then need to drag-and-drop your image folder into the GUI. + See the napari-deeplabcut docs linked above for more information about labelling in + napari-deeplabcut. + + import deeplabcut + deeplabcut.label_frames() + + Opening the images extracted from the "2025-01-01-experiment7" video in + napari-deeplabcut on Windows. The project's folder structure should look as follows: + + reaching-task/ # project root directory + ├── config.yaml # project configuration file + └── labeled-data/ # folder containing all extracted image folders + ├── ... + ├── 2025-01-01-experiment7 # folder containing the images to label + └── ... + + deeplabcut.label_frames( + "C:\\myproject\\reaching-task\\config.yaml", + "2025-01-01-experiment7", + ) + + Opening the images extracted from the first video listed in the project + configuration in napari-deeplabcut on a Unix system. + + deeplabcut.label_frames("/users/john/project/config.yaml") + """ + files = None + if config_path is None: + if image_folder is not None: + raise ValueError( + f"If the ``config_path`` is None, the ``image_folder`` must be None " + f"too. Found {image_folder}. To label the images in {image_folder}, " + f"give the project configuration file as `config_path`." + ) + else: + data_dir = Path(config_path).parent / "labeled-data" + if image_folder is None: + image_dirs = [path for path in data_dir.iterdir() if path.is_dir()] + if len(image_dirs) == 0: + raise ValueError( + f"Could not find any image folders in {data_dir}. Please check " + f"the config path given to `deeplabcut.label_frames(...)`" + ) + image_dir = list(sorted(image_dirs))[0] + else: + image_dir = data_dir / image_folder + + files = [str(image_dir), str(config_path)] + _ = launch_napari(files=files) + + +refine_labels = label_frames + + +class LabelFrames(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self._set_page() + self.skeleton_builder = None + + def _set_page(self): + self.label_frames_btn = QtWidgets.QPushButton("Label Frames") + self.label_frames_btn.clicked.connect(self.label_frames) + self.check_labels_btn = QtWidgets.QPushButton("Check Labels") + self.check_labels_btn.clicked.connect(self.check_labels) + self.build_skeleton_btn = QtWidgets.QPushButton("Build skeleton") + self.build_skeleton_btn.clicked.connect(self.build_skeleton) + self.main_layout.addWidget(self.label_frames_btn, alignment=Qt.AlignLeft) + self.main_layout.addWidget(self.check_labels_btn, alignment=Qt.AlignLeft) + self.main_layout.addWidget(self.build_skeleton_btn, alignment=Qt.AlignLeft) + + def log_color_by_option(self, choice): + self.root.logger.info(f"Labeled images will by colored by {choice.upper()}") + + def label_frames(self): + dialog = QtWidgets.QFileDialog(self) + dialog.setFileMode(QtWidgets.QFileDialog.Directory) + dialog.setViewMode(QtWidgets.QFileDialog.Detail) + dialog.setDirectory(str(Path(self.root.config_path).parent / "labeled-data")) + if dialog.exec_(): + folder = dialog.selectedFiles()[0] + has_h5 = False + for file in Path(folder).iterdir(): + if file.name.endswith(".h5"): + has_h5 = True + break + if not has_h5: + folder = [folder, self.root.config_path] + _ = launch_napari(folder) + + def check_labels(self): + check_labels(self.root.config_path, visualizeindividuals=self.root.is_multianimal) + labeled_images = (Path(self.root.config_path).parent / "labeled-data").rglob("*_labeled/*.png") + _ = launch_napari(labeled_images, plugin="napari", stack=True) + + def _on_skeleton_builder_destroyed(self): + self.skeleton_builder = None + + def build_skeleton(self, *args): + if self.skeleton_builder is None: + self.skeleton_builder = SkeletonBuilder( + config_path=self.root.config_path, + parent=self.root, + ) + self.skeleton_builder.show() + self.skeleton_builder.destroyed.connect(self._on_skeleton_builder_destroyed) diff --git a/deeplabcut/gui/tabs/manage_project.py b/deeplabcut/gui/tabs/manage_project.py new file mode 100644 index 0000000000..c64b02c4a9 --- /dev/null +++ b/deeplabcut/gui/tabs/manage_project.py @@ -0,0 +1,87 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 pathlib import Path + +from PySide6.QtCore import Qt, QTimer +from PySide6.QtWidgets import ( + QFileDialog, + QLabel, + QLineEdit, + QPushButton, +) + +from deeplabcut.create_project import add_new_videos +from deeplabcut.gui.components import DefaultTab, _create_horizontal_layout +from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.widgets import ConfigEditor + + +class ManageProject(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + + self._reload_timer = QTimer(self) + self._reload_timer.setSingleShot(True) + self._reload_timer.setInterval(0) + self._reload_timer.timeout.connect(self.root.reload_project_config) + + self._set_page() + self._videos = [] + + def _set_page(self): + # Add config text field and button + project_config_layout = _create_horizontal_layout() + + cfg_text = QLabel("Active config file:") + + self.cfg_line = QLineEdit() + self.cfg_line.setText(os.fspath(self.root.config_path) if self.root.config_path else "") + self.cfg_line.textChanged[str].connect(self.root.update_cfg) + + browse_button = QPushButton("Browse") + browse_button.setMaximumWidth(100) + browse_button.clicked.connect(self.root._open_project) + + project_config_layout.addWidget(cfg_text) + project_config_layout.addWidget(self.cfg_line) + project_config_layout.addWidget(browse_button) + + self.main_layout.addLayout(project_config_layout) + + self.edit_btn = QPushButton("Edit config.yaml") + self.edit_btn.setMinimumWidth(150) + self.edit_btn.clicked.connect(self.open_config_editor) + + self.add_videos_btn = QPushButton("Add new videos") + self.add_videos_btn.clicked.connect(self.add_new_videos) + + self.main_layout.addWidget(self.edit_btn, alignment=Qt.AlignRight) + self.main_layout.addWidget(self.add_videos_btn, alignment=Qt.AlignRight) + + def open_config_editor(self): + config = self.root.config_path + editor = ConfigEditor(config, parent=self.root) + editor.accepted.connect(self._reload_timer.start) + editor.show() + + def add_new_videos(self): + cwd = os.fspath(Path.cwd()) + files = QFileDialog.getOpenFileNames( + self, + "Select videos to add to the project", + cwd, + f"Videos ({' *.'.join(DLCParams.VIDEOTYPES)[1:]})", + )[0] + if not files: + return + + add_new_videos(self.root.config_path, [Path(video).absolute() for video in files]) diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py new file mode 100644 index 0000000000..d2eb319bcc --- /dev/null +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -0,0 +1,606 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 webbrowser +from functools import partial +from pathlib import Path + +import dlclibrary +from PySide6 import QtWidgets +from PySide6.QtCore import QRegularExpression, QSize, Qt, QTimer, Signal, Slot +from PySide6.QtGui import QRegularExpressionValidator + +import deeplabcut +from deeplabcut.core.engine import Engine +from deeplabcut.gui.components import ( + DefaultTab, + VideoSelectionWidget, + _create_grid_layout, + _create_label_widget, + set_combo_items, + set_layout_contents_visible, +) +from deeplabcut.gui.gui_assets import icon_from_resource, pixmap_from_resource +from deeplabcut.gui.utils import move_to_separate_thread +from deeplabcut.gui.widgets import ClickableLabel +from deeplabcut.pose_estimation_pytorch.apis.utils import TORCHVISION_DETECTORS + + +class RegExpValidator(QRegularExpressionValidator): + validationChanged = Signal(QRegularExpressionValidator.State) + + def validate(self, input_, pos): + state, input_, pos = super().validate(input_, pos) + self.validationChanged.emit(state) + return state, input_, pos + + +class ModelZoo(DefaultTab): + def __init__(self, root, parent, h1_description): + super().__init__(root, parent, h1_description) + self._val_pattern = QRegularExpression(r"(\d{3,5},\s*)+\d{3,5}") + self._set_page() + self.root.engine_change.connect(self._on_engine_change) + self.root.engine_change.connect(self._update_available_models) + self._update_pose_models(self.model_combo.currentText()) + self._update_detectors(self.model_combo.currentText()) + self._destfolder = None + self.worker = None + self.thread = None + + @property + def files(self): + return self.video_selection_widget.files + + def _set_page(self): + # Create Run button first so it exists for any method that references it + self.run_button = QtWidgets.QPushButton("Run") + self.run_button.setStyleSheet( + """ + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + } + QPushButton:disabled { + background-color: #9E9E9E; + color: white; + font-weight: bold; + } + """ + ) + self.run_button.setFixedWidth(120) + self.run_button.clicked.connect(self.run_video_inference_superanimal) + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.run_button) + 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.main_layout.addWidget(self.video_selection_widget) + + self._build_common_attributes() + self._build_tf_attributes() + self._build_torch_attributes() + + self.home_button = QtWidgets.QPushButton("Return to Welcome page") + self.home_button.clicked.connect(self.root._generate_welcome_page) + self.main_layout.addWidget(self.home_button, alignment=Qt.AlignLeft) + self.help_button = QtWidgets.QPushButton("Help") + self.help_button.clicked.connect(self.show_help_dialog) + 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 + self.go_to_button.clicked.connect( + lambda: webbrowser.open( + "https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models" + ) + ) + self.main_layout.addWidget(self.go_to_button, alignment=Qt.AlignLeft) + + # Add the Run button layout + self.main_layout.addLayout(button_layout) + + self._on_engine_change(self.root.engine) + + def _add_supermodel_section(self, layout: QtWidgets.QGridLayout) -> None: + # --- Supermodel selection --- + section_title = QtWidgets.QLabel("Supermodel settings") + section_title.setStyleSheet("font-weight: bold; font-size: 16px;") + model_combo_text = QtWidgets.QLabel("Supermodel") + model_combo_text.setMinimumWidth(150) + self.model_combo = QtWidgets.QComboBox() + self.model_combo.setMinimumWidth(250) + layout.addWidget(section_title, 0, 0, 1, 6) + layout.addWidget(model_combo_text, 1, 0) + layout.addWidget(self.model_combo, 1, 1) + + def _add_pose_model_settings_row(self, layout: QtWidgets.QGridLayout): + # --- Pose Model Type and Pose Confidence Threshold on the same line (now row 2) --- + pose_model_row = QtWidgets.QHBoxLayout() + pose_model_label = QtWidgets.QLabel("Pose Model Type") + pose_model_label.setMinimumWidth(150) + self.net_type_selector = QtWidgets.QComboBox() + self.net_type_selector.setMinimumWidth(180) + pose_conf_label = QtWidgets.QLabel("Pose confidence threshold") + pose_conf_label.setMinimumWidth(170) + self.pose_threshold_spinbox = QtWidgets.QDoubleSpinBox( + decimals=2, + minimum=0.0, + maximum=1.0, + singleStep=0.01, + value=0.4, + wrapping=True, + ) + self.pose_threshold_spinbox.setMaximumWidth(100) + batch_size_combo_label = QtWidgets.QLabel("Pose model batch size") + self.batch_size_combo = QtWidgets.QComboBox() + self.batch_size_combo.setMinimumWidth(100) + self.batch_size_combo.addItems([str(2**i) for i in range(6)]) + self.batch_size_combo.setCurrentIndex(0) + pose_model_row.addWidget(pose_model_label) + pose_model_row.addWidget(self.net_type_selector) + pose_model_row.addSpacing(20) + pose_model_row.addWidget(pose_conf_label) + pose_model_row.addWidget(self.pose_threshold_spinbox) + pose_model_row.addSpacing(20) + pose_model_row.addWidget(batch_size_combo_label) + pose_model_row.addWidget(self.batch_size_combo) + pose_model_row.addStretch() + layout.addLayout(pose_model_row, 2, 0, 1, 6) + + def _add_detector_settings_row(self, layout: QtWidgets.QGridLayout): + # --- Detector Type and Detector Confidence Threshold on the same line (now row 3) --- + detector_label = QtWidgets.QLabel("Detector Type") + detector_label.setMinimumWidth(150) + self.detector_type_selector = QtWidgets.QComboBox() + self.detector_type_selector.setMinimumWidth(180) + detector_conf_label = QtWidgets.QLabel("Detector confidence threshold") + detector_conf_label.setMinimumWidth(170) + self.detector_threshold_spinbox = QtWidgets.QDoubleSpinBox( + decimals=2, + minimum=0.0, + maximum=1.0, + singleStep=0.01, + value=0.1, + wrapping=True, + ) + self.detector_threshold_spinbox.setMaximumWidth(100) + max_individuals_label = QtWidgets.QLabel("Maximum number of individuals") + max_individuals_label.setMinimumWidth(180) + self.max_individuals_spinbox = QtWidgets.QSpinBox() + self.max_individuals_spinbox.setRange(1, 100) + self.max_individuals_spinbox.setValue(1) + self.max_individuals_spinbox.setMaximumWidth(100) + detector_batch_size_combo_label = QtWidgets.QLabel("Detector batch size") + self.detector_batch_size_combo = QtWidgets.QComboBox() + self.detector_batch_size_combo.setMinimumWidth(100) + self.detector_batch_size_combo.addItems([str(2**i) for i in range(6)]) + self.detector_batch_size_combo.setCurrentIndex(0) + self.detector_row = QtWidgets.QHBoxLayout() + self.detector_row.addWidget(detector_label) + self.detector_row.addWidget(self.detector_type_selector) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(detector_conf_label) + self.detector_row.addWidget(self.detector_threshold_spinbox) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(max_individuals_label) + self.detector_row.addWidget(self.max_individuals_spinbox) + self.detector_row.addSpacing(20) + self.detector_row.addWidget(detector_batch_size_combo_label) + self.detector_row.addWidget(self.detector_batch_size_combo) + self.detector_row.addStretch() + layout.addLayout(self.detector_row, 3, 0, 1, 6) + + def _add_output_settings_section(self, layout: QtWidgets.QGridLayout): + loc_label = ClickableLabel("Folder to store results:", parent=self) + loc_label.signal.connect(self.select_folder) + self.loc_line = QtWidgets.QLineEdit( + "\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", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1/1 [00:00<00:00, 1.95it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 78.27it/s]\n" + ] + }, + { + "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, strict=False) 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, strict=False)))\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 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", + " 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": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAGuCAYAAAAAg7f4AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9eZQd2Z3Yd37vjfWt+V7uG/atUBuryCo2yWKRLLLJbvaqlnqVtc7YY43H5+jYGnmsM6fH9owkz8yZ8TrHkqUz8qgtS7aO1G6x3YvkZpMi2exmkbVvqEJhTSQSub58a+z3zh/xMpFAZaIyE4lEArgfnCiggHzx4kXEi/jFvff3u0JrrTEMwzAMwzAeGfJ+b4BhGIZhGIaxv0wAaBiGYRiG8YgxAaBhGIZhGMYjxgSAhmEYhmEYjxgTABqGYRiGYTxiTABoGIZhGIbxiDEBoGEYhmEYxiPGBICGYRiGYRiPGHvbPykEGtBHazhXW/duiw4CbZPHxuJ+b4mxbzSQgDB10Q3DMIwHV5Zl2/q5bbcArt0WX/vzP72b7TEMwzAMwzAOiO23AA4WePff+mk+/bf+yT3cHMMwDMMwDONeE9udC9iyrHu9LQeH6QJ+BJkuYMMwDOPBt+ddwIZhGIZhGMbDwQSAtzMNQIZhGIZhPOS2PwbwINN72V0r9nBdhmEYhmEYB8/DEQAigL0eo2iCQMMwDMMwHk4PSQC4xgRthmEYhmEYH8eMATQMwzAMw3jEmADQMAzDMAzjEWMCQMMwDMMwjEfM3Y8BvO9lU0zWrrGH9vp8NqemYRiGcQDtQRKIxf1vSDR3WWMv7GVOlAbSPVyfYRiGYeydPbjjCUwrnPHg2+tzWO3hugzDMAxjb93vpjvDMAzDMAxjn5kA0DAMwzAM4xFjAkDDMAzDMIxHzN0FgPc9A9gwDMMwDMPYqe0ngeitYkWT/GEYhmEYhvEg2UEW8J1+1ASBhmEYhmEYD4odBIAmyDMMwzAMw3gYmCQQwzAMwzCMR4wJAA3DMAzDMB4xJgA0DMMwDMN4xOzl5KeGYdxuq1JJZkitYRiGcR+ZAHCHhLh559baFEI0tiLY+uulMHMFG4ZhGPeTCQB3QQhhgj/jYwjA2uTvNaaCumEYhnG/mTGAhmEYhmEYjxgTAN6Fjd3BhmEYhmEYDwrTBbxLa8Hf2u9aa9MtbBiGYRjGA8G0ABqGYRiGYTxiTAvgjsnNx/DfaWy/MC2DxkYC9FbDB7QpEWMYhmHcc490ALjzMXwStLXlDXrL1YmM/Sr7sd1uaFPO5n6SbN74roEUkyVsGIZh3GuPdAAIOwwCNf3g76Ov2Xo1+uN+YM/sJJDbOHbR2E+mec8wDMO4/8wYQMMwDMMwjEfMI98C+LDZr65d04VsGIZhGA8uEwA+RPY7KDPdyIZhGIbxYHo4AsDdxB9rY/n2K3bZr/cxQ8wMwzAMw/gYD34AqCGPenYY+Wixi8SM3UZXkn2LALVa38yt5iy+PfFlp3Mb3+3rjTuxYNN9qQFlAnzDMAxjTzz4ASCQ3xV3ms/Sr8W2LzfUXQSou3IzcLh9ppKtbPfnPm4dZkaUvbLVeazYr1JChmEYxsPvIQkA1+wmiLnXgdl+NdmYoOvBZ5r3DMMwjP1hysAYhmEYhmE8Yh6yFkDjILtTN7PpNjYMwzCM/WMCQGNfbRYEmuDPMAzDMPbX/geA9+Reb8ZO3eJ+x1PrU+YZe86UEzIMwzD2wH1qAZRsHH54NxmosFY1435HPQeB4L436moFpDt+2VbngGkdXCMBZ5/eK8V8nwzDMB5u9zlaEOu/300QqLXCNFkclM+/8+34uGNvgkC4H6WEDMMwjIeXyQI2DMMwDMN4xOy6BfBuB/MLcX9iz7vtbjb23scVjzZzDhuGYRjG3tpVALhx5ofbbe8mLfqzsO1vMHan7TYOro3HzASBhmEYhnH3TBewYRiGYRjGI2b7LYB6Q6woxOZjxbXYYiL7W+11K5wQwrQMHShy8/NDsOMSMabF9n6Q2/oe30qDeHi+g+ufZKvTTx+ctCvDMIzd2HYAKMSGH73DTfx+3a9NoHBQiFvPlVuo/rLJq7Z5/Ex38H6wdvGah690jJZsfp17uGJdwzAeUTsIAD/+Bm2CP2PrY/HxSR47WbcJ/u6V3XyXHtLK31udYubUMwzjIWCmgjMMw+CjDxUC0DpPWNP65sgXoXX/Dzez07ebFGceVg3DOChMAGgYxu5pzQga0PTQD3TjmNa3br8jLZRSSCGxLItyuUQQhkghybRaL1+klCLLMoQQSCmxbZsoitb/fa19VIjtjZE+6Hpw/7p7DMPYM0Jvsy/Nser3eluMh5pmfQzgHtw7Pq52oLE/RrTmBuH93gxjH30P+KIQJgg0jAMqy7Jt/dzD0QL4IMQB5lp5054cr7WUYsMw9tPngSL9lkDDMB5YD34AqCEvW3GASxqKDBOsQB607SbDdAvapGPebxuDgHEcuh/5CQ1kB/gBSCMtgURiS4nvOAyUKzzz9Cf46pde5OzZkwwODuL7BbTWfPD+B6y2miTAqZOnGBoaxit4DNTqBL0eS6st/uSVV/hH/+h/4O233yaKYjSaLM36LdYHdkd8rBJww7S6G8ZD48HvAtaAttjTwGKvicQEKsBeBsFag9Zmv95vRa1p97uAK3j0Nu0WPMjHSeF5LkXfZXxohM986nle+vwXGKnXce2UVnOJgYEBqtUqFy9e5Jvf/CarzRa2X2KgVufEieOcPn2asdExLMuiOjqGWx0g0/Dtb/0hv/nPf5Pvfe+PiOOIXpjAfZoCcy8Utabdv11UhNjiWBuGcb89Wl3A6w7iBemg3vjuh708Pma/HjyCjx7jg32cXNthYnCA5z/1HF/90kuMD40QdwPefeN1rl+7SBi2EULQaDR44oknGK7VWbqxSJoJwl5IFsWE3YDjx49h2zZv/vb/Qn18nKeeeoqvf/klHj9+nH8wPMhv//bvEEbpFlUwDcMw9t9D1gJ4QANA0wK45/IkELNf77dbWwD9TVqFNPe7BXBjmZbby7BUCy5/+Vf+NH/2l3+FLIw59/Y7zFy6Qme1ycrqIqutBo5to9GkSUqhUMD1CzS7IZZt55m/QjI6Nopt27SDHp0wYGBggCeffJKvfvWrtDsd/s7f+bv8k//lX9KLk/Xt2O5T+kFhWgAN48HwiLYAfpSUcv2ir5QymaOG8QgTQmBZFq7rMj09zb/xS7/AX/6Fn+bqhYu88/qbLF6/wcrcDYJOlzgJ8YTE6k+ZXir6SClwpY0ulkjTjCxNyVTCyvwiURjiFz20SlhcXeE712Zo3bjOL/3yr/Crf+rnuLLY4M33z9PpdOh2Pzpa0jAMYz899AGgYRiPtrW5wtceBj3P4/HHH+cXf/EX+Yu/+ou884e/z7m33ubKhxco2i5ulpGpjFSDUuA6FoViAa00lm0jpYXnF+gFAd00w5IWpBlDtTorjUUcW4BSJHGXl7/3XWSa8FM/87P82V/9FVb/u9/gypUrJElCFEX3e9cYhvEIO1gB4MfOwL6VO//8/W/1E/s0FEqvv92jwZSCMT7e2vffsW08z+OrX/kyX/zCF/jpn/opXv/+H/HOH38fCxiuVoh7ASXXxkodLMeCMIAsxbMspC2wbBsQKCHwbRtcF4AkSUiCgKpfAFK0bROhka7LO6+9TqVY4omvfJ2f+MpL/Jf/9f+HOM4LRW+spffIfG0NwzgQDlYACKBtYO8y5ZQ6AMOu9X7tZgUi3af3uv+E3CzpYGfu/8OBcc8JkEJQKDi4OuPwYJlf/fpLdBdnmH/zR1STCMuyiEgJRUbmgi9dnDRGWhZpmuLbKbVajUZjhaGhUVZWQ1AZWmckcULZdeh1e1ieg3AclNIILcmSvGD5u2+9g1et8cUnH+Pt5z/J7//r79FDoUV+DgtzHhqGsc8OXgC4aSbhg2y/PsujdQNZbzi5i4HoJvh7ROQT+KLSlPHxMX7tV38FGcdcOvceVpYyUC4TRxHYNsXawPp5EaqUVreDEAKlFTaasu+zMHedysA4trSIwgDHthBaI0T+bbdth1arhdZQKBQIgoDVxipzMzOMjk3wtS99ifc/vMgH12aJFSAl8GAlhBiG8eA7gAGgYRjG3hGAIyWTo6P8zNe/zskjx/jwjdcI210826VcsYhdd32MoGVZWJZFmCZ4Kyu4rsPMzDUKRRfPcmivtgmCANvJu3/X5gL2XJd2r4tvSSqVCr1eXia7WCwCMDczS3XgQyaPHefMiZNcnZsnydL+GMX7tXcMw3hUmQDQMIyHnittyp7Pp5/5JB++/S5Ls3OoKMaRFjpL8X2farVKGIYUCgUKhQJRFFEpFAmjiHZpFd92EUC9MsBiJ0KTB4tZlq2XdUnTlCgMKZVK2LZNkiRIKSkWi0RRQtTp0Wu1+eRTT/P9H75CkoWkCNP+ZxjGvntwy9IbhmFsk05Tio7H0ECN61dnCNpdFmdvsHRjnvn5ebIsw/d9HMdBa41lWUgEBcel7Pocmz6Mb9lIpSm4HqVSCSnlequh1hqlFMViCSEEvV4Pz/Mol8v4vo8Uglq5QtTposKEU0dP8MTpMwilEJl61EZwGIZxAGy/BdBcoB4ce3msDvpwzDt91oO+7ca+EEDB9Zgam6CxuMSF9z9A97qszF2n5FqUPEltYIAgCHAch5WVFYIgQCQZjmXjex4DoxUcabG4sABphkCSJDFCCIQQJEmCbdsUCj5hlqK1JgxDyuXyelDpS5c4TknDiKpt89Lnv8hrb71LkGWkyrQBGoaxv3bQBbxfc+2au/buCfb2OKl80t0De0gkW0eAByD72+iTbD7ITe/PDCECypUKh6an+fDdc8xeuoKOQxYW5xiplymO1Wl3m/gdL8/0ba2QJgkyTRisVimXSzhOlXrdp9ezYCWi01PEqUJIgdKQZgqEQugUy8uDwjDuUNIuvuMikKRhhGu79FqrFJoVPnHmDEPlEkutVn8uFcMwjP2zgxbA/QoAjd3pR2l7eZwEHOzsxK0yxk1z9cGy1TmpgP0oWyRQaAqez/x8RNf7MVa710jtZZpZiFuwCZMetgut9gqLyzdot1tYOuPG8iip+CRjIwPUBz6gl7TpZj2iRIBw6Ha6FAo+lmPT7faQIsKvubiWje/YWDLFsRS+49JTKm8tjHoUbMlwpcTZw4d4+a23TEVLwzD23Q5aAA9sM5Cxbi+P0UG/Hd3psx70bX+UbHWc9vEYCfB9n8h/hh+Wfo2s7MEQiN55Rlt/neOHD1EulxgdGeXS5ctEvYBeu8tC6wli/j5al3nnElQrDV764v+XunqbmaVzKJ3i2pIo6JGEIRIFShJFGdK1cWxJlkISZRRch2qlQJwoEgVxFCGAz/3YZ3n1vfcgfnTqdxqGcTCYLGDDMB5qWms6PcUP279GJvLSLdpR6OoRLlp/Dbv4u9THJpC+T6k+xOD4JHZxmCvRf4NSHsgMkUpa7Srf/eNf4sUvBpzohLz95pvrs3nEQY8wjNCxh0WZNFREIoISRJ2YKEiZnJzAcSzSOGVlpcHk4Yyzj53Ftm2INeYh2zCM/XTgAkAp9y8x+UDMEmIYxr2lQbmHSbWXD2sVmgtfa1E+28HyjvJX+Xc2f5lepdXukoSS8X8xRGVFMnppkieWVyl5DhMjg1y5chmtNUJpVNRjbmmFeNFlbHiEsucTdxJsJL1ShFKao8eO0Q1jlFb0ej2OHDnCxMQk87NXiJPEXJMMw9g3By4AhHzy9nvNzAJhGI8IAVLnRZllCk4smf5mlQtCMvxkE7nJEMUkFaw0PJJUUlhwyFxYHVesjsONzv+Wz5e/x49/4QgnZy4yf+MGSZLQ6fZw51d488oc12bnOXn4CLYlyTS0Wl38Qp4N7LouaZLQ7XaoVKt89tM/xpVvNYmSBIDV1VUTCBqGcc8dyADQDOE6IHR/bqu9Ph4HYXY809t2MOzHMdLQWz3HqP0ejeAsAO16RtKxKf+rhP86+O8ZHqhh2zZBEPCbw4/xX048TyIlTlfwxO8VGH7fYeFoRuNwTKt8gt/lBN9Je3zt1Lt8+fEfUU6X6XS7vHHxGivJj5i9Nkur2WXs2HGk0gSdDo2VVRYXF5mYPkwvymg1myRxxGc/+xn+xz/8V8RxjJQSpdQtD6j78UBsGMaj50AEgBu7fS29P9Wp8+vr/nQ3J6gHNKaVoJ29W53I2J/yLHcqh6Nv2YaNN1fTKrzfJLDZ+aXJs8/36ngIpLQ55fwGr+m/TYbgUNRjTlucr47zN7/3H/Dpf1bEfewG3/5PFvnjsQkAPtO5zld/O+LGhRNIt8GfKr/K0drrfCd5ln8ZPceCqvGb7ef4LT7JZ+x3+Ir+DofL1zk5NcTCqZ9lvjSOJ1Z4MnoHyw5YWlhlaWmZY8eOI4BqySVZXeTIUI3jYxNcvHqVDE0XgZL5/MMP6IXDMIwHwIEIAKF/I+6XnJP7ctHbn6dq/cBewcVtv9+t/dwPdyoPs3nLign+9tudzq+1IH1vjokA9Nkf45vTf5qBRYnrp/ynn/lt/uHbj/P3jz/G935KEzgR7/yyQ1CfwFUZ/6fGm/wVNYP9VU325deIwpBeLwAkf47X+fPZG/xR7wT/vP0Mb2dH+X76FN/nKSZqV5j92gA9vwgo3rUcwivf4HMX/1viMCaKIprNJlJKQtsijboMjozzqaeewXFcFldXcDyPIIlo97okcWyCQMMw7okDEwAahmHcC/qzX2f5b/wD6m/6DABXxxT/1D/F6X8xybNnXV77yZhXvh6ja5rhD23+kzff4k89dwXHl+gsQ2cZQmmcfk+FACwp+Zx7jsfE93knqPJN+yV+aH+aOesIMoFiqokLmtTSXDzyczyx+kfU43cJgogwDBkdHUUpRRzHKJWx0lih4BcIgxDLkrjSw8tSsjRDZQe5FqdhGA+qfQ0AH9WxLKal6cGwNqer8eBbO46WZZH+O/8PAOpzeQmY5cmUv8Vp/jNSXvwnPnYscGVGb9Tihf+2wsQvdXGkha0FSuc9E0qDyPT6+DwFBEFAEASMBAv8ij7PT4t/wt8c/Y/pZUO4oeDQ2xa9mmbmiZRWcYrjo0t0ugFxHOM4Do7jIITAth2qlQqvvPEGnU6H5dYq2pIokVcqeDSvmoZh3Gv7FgCuTZr+6BFIIdD9j377AG/jYFg7N9d+N1mYDwfpOFAfw+sJnvi2xcpUxvJ4QiYk7rNXkK9N8MI/8/s/rRBWCtVXuHIu5PDRaZTIyLKMLE3RcYJK83l7wySm0+kQhuH6uVLUTab1B7xReY7adZfp9x0SV3PjRIrXnSPLMmq1Gq6bB6KWZa2fby+88Hlee+cd2kEPgSDNFEqYXCXDMO4d0wVsGMZDSyiFWJjh6PljHH3bobKSEVQUrsp45slv8uqTAYtvfxkAJQPE4/8B7fgGs5dtSr6DW/RI05Q0TYnjmCzLSLOUXhoThCFRFOV1APuB3M+t/DPeKz7F3CnN9DmbSsPiie8tcqj4AXaxjuu6eeHnviRJCMOQgYEBnn3mGbxigfkfLplxf4Zh3HPbDgDv9klU6O2tQ+j9fObdn6us2PBW9/rT3Y/7hkYjNnyytcQX8ZFPKwG9i43c5sljGLcRQlD8b/46J4d+C4CLn4yRwP/5+rcppAnP/ZlvsPj536LZ9agfjSgXLUbcUxQiTZolpEGGyvKxelEUkWZ5MBhmCUmSkiUplm0h+4lHU+k8v774N/nH4SeZGR3j8cZPMHR1imB6hIFShmPbOLaD0KDSjDiMiIOAqdEJLK0Jux0sBBmi/z0ykaBhGPfGtgNA725Lpmz7WibZn/IsirzUxL1n3dKbuFWG6t1L0aT36YYhhMCyLCzLIssykjT56Jg6vZtjm/XLx+yv24crmG77B1f25vc4rRtAmdPPzvIXu+8yMvMG3TBmcLRC8dm8/l7RK+NbNjLTqDglDTPSOCNIY4I0IlEJcRyRJAky1UjAkzaOtBEyP1+yNONIuMQLb/9dfvuPXyc98fvYfIHVpX+TqbG/h6MlrrTIwpg0jEiCCCFspuujPHv8OK/80fcp4CKAUCmUSEGY4QiGYey9HbQA7lcTzL0LkO6Hj36Se/PZ8plE73+QYlkWjuOgerePddzN575/86OaxJ0HmxACKSVpmlLXxxjIDoHM+ErtXzHaEsSuSxZGCGXTmmlAlvHB7A08aXHq2AkG64MIEZPoHrZtUyqV6PV6/XNBgE5vFrIRon991Fi2Q5Ap2r0ApTVzrf+IQ9U/IAq/QGPlmwwPtpFCEscxnudhSYnqZxpPjk8wPDiEuDKztmYepmuhYRgHixkDaOypLMuI45hUpVhiq2LMhnHvZf3yKWfsn4IMameW6ASLtOebPHnkCMthj9/8l3/ApZlr1KsD2FpAmvH9l9+n6HmMTpQ4eWqaqakpLMtaT94QCLJMr1WTX6cBJSQr3S7XV1bpaQhabzIx9FvYyZ9hdvbPcfrU38P3PXq9fGo6y7JI05Rut8vExDiHDh1CvPEGZOahwzCMe8sEgA8RIfKM453ai4xXrfX6YPm1MYEPU8vZdjLYH6bP+6DTWq8nZxxJvwLA9HPLqFRxfWGB559+Enu1gDs6yC//yi8yMjRMyfUhSYl7AR+88y5/8vu/SbuzxNTUFLZtk6YpjuMghSDJNGmSEMcxwHpiRyeKuN5Y5YPZeTqZptfu8P7y/5UnBn6SOD7FtWtPc+zYO2itybIM3/eRrosQkmKpxOdf/DzffPlPuHxjAc/1iFKF1qYOoGEYe88EgA8VwU7jv70IWm4fHrB/wwX2x07KF5kg8GCR2uYwXwLg0LPLXFiIWFpcpNnrUBgc4NTjp7l0+QKt1WV8x+X8O+8RtNo0l1cYHRvma1/4DJVKhW63C/S7e/tjW7MsLxGjtUZKmT8EIZhtrLIYRoTSJ9SK83PnmKz+Xer8NT44/3VOnbpMp9OhUChQq9Uo1+po36Pg+0xOTvLFF79E8O1/zezyUv+97uMONAzjobU/k+Ea+8fcLLZH73AxHkjTfBaPCspdZfxYC6mh1+vx4aWLFAYqnDpymOid93j9n/5z3vnNf4F14RLVGws84fp84ZlnqFardDodxIb5yvPgL81LwqRrv2ekWUan1+Py7BxKCDJpkUqLbpry6tW/hZA3SJJhzr3/6XwquDCk1WqRZRme51EoFqnX6kxNTTJYH2RqcgpLWua8NAzjnrjHAaAErB0u+0XsYtvutNz/WFrofB5lqXaw6JutGrcvD6fdHHe57ZvtVvvy4d+ve02Ctna/YAM2p/gpAKKh18mEomg5uIlm5v0PKTguSdjD1RFT1RLlMKQSxRwdrPOJs6cYHR1CW+AUXKI0ItEpURYTpQlhqogVpEiiTNOLE3pRwlI7YLEZ4FgWQsXILEFKi+Veh9j6rwA4f/6LaAbIkpAk7NJtN4iyLlHSRtoJ9VqR0cEyU8MDjNSqSEBohdQaWwg828ECbDvPul/r7l6jtTYF5w3D+Fj3uAtYcnCz2PY6w+7+lmpY+yTWDq/5GoGWm0858PDdRO7tubjd4O7h2qf3yt2XnRLAcb4KgHvyIrY7hQoT3FjRnFtk8foNTn3iMVqri1x+5U28coXDYxMMT09QHh0itfMSLGmaoATEWUoUR0RRTBinqCwjzTRaC1SStwZ2wpRI5dPH2VmKVIpMWMRCshz9U46U/jJRcIrLl3+K08d/A5HFFD0L1xfYjqJUcpmeGGawWkDolKJjM1iu0gt669nGmc4ouB64DpnK1pNd1jKf186vjd9f8+BhGMbt9mEM4EG98Ozldh2MG/puC608Wna6lx69PXT/7cV3U1NkmAmeAWD42SW80mm01pSKJRIiZq7NcPqZx6jVa3z28y9QSgQFy8Eu+sSuABWhUcRxSpZq0lQRhjG9bkAa5QWi18YAri2Zyv9OAJaUsCHBKgwCKof+DtHsf8blyz/GkalvEQSLRGFEvNKgVK4yXKtzeGKSF37sM3xw8RKHjp2g2e2ysrJCGIbr4w47QZd2GBBGEQClUimvv5kk67/f3jJoGIax0f3vtzSMR4xpjdkfx/kyAsmCeAtZ6WDZNsVikepAlanJSVSmCKKY8UPTlOs16lMTVEaH8QYq2L5PpiVhkNLrRPR6MXGYEQYpQZAQRdH60u12SZIEx3HWC6HDR49znMRo8UOqtZfRWnLu/C8gpaTX7WIpiLo9wnaH8eERPvH4kzSXVtAqo1QqIaRAaUUUR1QqFT73mc/xsz/zszz33HNMTk5SqVQoFAoIIYjjeH3aus22wzAMA0wWsHEHt49Z24tyMUbOFJq+t6SUnNRfAw1XrG9xKkmRaBzHoVQqMT41SlsHNJYWGR+rg+8ihYVOM3pRwGoc0Gw06TW7dHtdwjAkCiN6QY8wCNBJvF4HUEqJ7/uUy2XsVo8sUyitidNby7ekaUoURQwP/wPazU9xY/4sS42zFMszkGQk3YBSoYrIFEcmp/n0s5/k7/3jf0xpaJBr167RWG0QxzECwfe+/0dox1pvn7Ztm1qtRq1WQ0pJp9NBKbXeDWyCQMMwbmcCQGNLJki5N8x+vfe00hznywC0Rl5Hq1GyJKFarVKv1SmVimRK0G63USLEakeEqcBS0Ay6LPVarC62iNoBURyTxDFJkrf8pUmMa4FtSex+q2K5XMb3fXzfx3FsdBLlx3nD8Q3DhCAISMsfMj39HWZmXuLNd3+GE8f/O7I4QScZlgZXWvTaAV/54ku00pR//nu/i23beSC31j2uIU2z9d7yJMnXXSwW8X0fz/OIoijvjjbBn2EYm7iHAaC56DxQTBzy8bbaR+ZUP3DG5FNU1AQJAc7xeT680CaIE4bGRugsL1AolUgzQaPRINUFmlfnEO2AqBvQjgJaSUjaTSHW6+VetNa4rovr+jhWhuc6eK6L63lYlkSj86kQbQetQ6SQbJykMUwzukFINwg4Nv1PuXHjM6w0xrlw6Wk+NX4DlaZEvQDhFtBZRhQnfPq55wkF/NEffZ9Op01X9da7dpXKbjklpZT0ej16vR5SSpRSJvgzDGNLOwgAdxMr7ubis9clVVL2J7o5yBnPABmb7QcB2Nvu2c2PiwayLSqjPJwDzwVbn/+b79cdrd20CO65Yypv/bvh/4BmZ5l3mtd49YNzPH3yBGm9glsfwFvVZEFGc6bJ7Mwy89fn6Ha6WLaFAqJuF8fK5+11bJtCoUChWKE6UKFiZ9iWhW1bSCmxLJCWwnNdtLKxhItcqzSgLUDQTDUN5UAnotq7wakz3+Tdt3+Gl1/9Iqee/v8xPlBEWxmuL/GUxUpnlagdUNCS55/6BCcOH2Fm7jrnL13g+vwN4ij8SPmXtd/XgkQwYwANw9jcDiItuYtlNxcescv32qv33421kjJ7td17vWy9H7a/1eKWJZ915FGocyfusOzB2h/a/XZ/neDHAXg3+h3efvcdLly6zPe+/wO6UcLo1DTadnC8AqQQdkLarS7LjSbX5xeYnD7Ml7/8FUqlEotLi0RxxMpqg27Qw7ItqtUKAwMDDAxUqFYrFAo+jmMhBSAEWgsQa9+7fNFIMg29JKEXJ3SDgErtt6lWV+n1yrz+6qdI0oQ4iUBoCsUC5XIRx7Y5dvgoaZxQr9UYHxujXq9jWXnN1M3On4f7+2gYxl4xWcCGYTxUbF3gCC8AcFH8IVJIklTxw5dfYWm5ScGvILTNyPA4tuPh+4V8jl+Zt/YtLCxw+PBhXnrpJSYnp1hcXFmf51oIgeO6eL63PuYvH/fnfGTM3+3iNKHb6YLWtDsdoqjNJ5/9JgCv/eg5lpclQRgAUC6XqVYHUEpx+NAhnnvuOc6cOUOaZQS9gLGxMaTcz8L5hmE8bO5pALidWRFuXdbal+7ul2E8qEzLzd07wuex8WkywxLn8vF7yqJeH+XwoWNUKjUKXplKfRRLuti2TblcxvM8isUi3W4Xy7IZHRvjT//pX+BnfubreF5hvbxK0OuRpinAeqYtAEKAyOfjlkJgSYlt2UiZt5mnmaLdbhMEAZ1OhyiKGB35ERPjc6Spw/e+9UmSOGFhYYE4iqjVagxUB7hw8SJBENBoNKjX63z2c58l6U9Bt7EGoWEYxk7c2wAQgRRy+wsCKXb4mtsWEwAaDyrTdbc31rp/L1vfwnEclFKMj0zy6ec/x/yNZaTwsCyPTqPDyPAYlmXhui6e5+G67npdv3K5hO/7PPvss/zyL/8ijz/+eJ4QkmXrGbYbgy9L5lnBtr0W9AmktBAiv8wKIMsy4n42caPRIIpCXvjctwF47+2z3LheIUvzn8myjKnpKcbHxpmamuTy5cssLy3x4Ycfcvz4carVKp7nmXPFMIxdMV3AxoNBb7EcBAd52x4ha7t9LQD8QP0+cRIzPDzCmTOPc+XyDCvLTWzbp1QaoLnSoj40SqlUwvM8KpUKruuilCJJYtI0w/d9siyjWq3yqU99iqeffhrf93Fdd30cntwQ+LmOi+t6SNmv0bfhPLCEwLYt7P7r2u02q40Gw0OXOf3YBUDwr//XT5FmGWmSIKWkUqnwxBNPUCyW+NznPsfZs2cZGhpiYX4e13NxHGc/d7FhGA+Re1YGRqylF+idPp3u5dOsIM8q3undWO3iNQfdWkmK2+0uWpF681eJO6xqd2Wk19JU9m6Ne2urhBCdjwe7i9NZCGG69nZCa6pMM8JZFBnX3O/jWz5PPf00V2/MUpg+gtKKOI6o1KvIRoFAprgVH7vo4pY8LMciCxI6nTYDxUF0liJ0gudIbCkpjtTRQxVEEkB/XVppRH8OXumA42mE1EgEjhAoFMLKsJXGtiRKJQjLIUojbqwsMN2b4IUXv8OH548yc2mKD96pUKq0KFXLCMsmDnoMD1dodQs8duoEtVqVsaEhfut3foduHGMh1q9YZu5fwzC2a89bAKWQWNLqd8fasOPl7i5cQggsYfUXiSUcLOHucLGxpPWRRYoHucFUkgfDmy07Z+m8fMxHFi1wsTZddr731spo2JsvB6K7f6u86bsfoC+EQEqZdyeaG/rHE4ITIm/9u86PEMWI+tAQs/NzzLUWyZyMUtlFOgrta6onxugWFZWJQdxagcxWSFeAVLRbDSwSXBEzULSwdIArYzwroegJHM9BOjaO7+P4Hm7BRzo2lpfh+jG2yPCkQwGbiq2peSkVz0IlMVJk9MI2qUiYWbjGwuo8A+Xr/Nhn3gHg+998nnZjkW5zAb+YMTBoUSwqjh8Zo2Aplmau0l1a4ue/8jUeP3qSomXjSglao8UunrkNw3gk7cNMIPfzarSb934YW1zutB92/nn3d69u9W4H4Tjt7X417pLW692/F8U3KRdKjI+MIQQEzQblos/oyBDtZgPHHaBY9Oi0NeVKBcd10YDjOlSrA1SqFbRWCAGOY+Nlbp74oTVZvyi0ZVlYlrXe/ZukKUMCxgZrtJZTRCZQaYpQCi0UY8ODlDyPsNvBLbo4lk0WxywtLhIdmeKLL73K66+eZmVpiD/+zjif/+oSzkCFQr1GfbCO60Y8/niJbickSVLeevd9HMumVCqRdrvYQpAJjdIalDn/DMO4sz1t0jIJGIZh7Jfbu8d92+dEf/q3+cIfcebEScaHRpifvc7kyCDPPv04Rc8m7DSxUBSLHiqN6XY7CCEo9Mf2VasVyuUySimklDiOg+d52Hb+vLyW9WtZFp7nUSqVKJVKlEslJkdHOHP4EKPVMgUJns7wdIajFN1Gm4FSmcMTU7jSxrNsiq5H3A1YXFhEiBZf/trbALz+Jy/QaqY0m6ukaYrnuVSrVfyCz9Fjxzh69BjjY2MMDg0xOT7J2NgY1YEqruuaq7BhGNuy7RbA7XZ/PuxdVbvpBlb6IIxVM4yHy1q2tJSSwcFBxuNn8FfrpFaHn/6LTzMze4WFxUUmR0f587/2Z5isVqmWC5SdYeKoy40PrlGrl4lXY4aGBvlAKdI0ZbA6QLlcxtYRts16K58QgizL1ufXXWsBdF03H/8nJUXb5akTx5m/sopMPcJUk1kRnbBBnGriTo+R8VHcLEFmmlLRJw5Coiii1Wrz3I+d44+/d5qV5Sq/9z+P8emvv8NZz2NkRGJJj8pADcdd5tChQ3xw/iJuscjFq5dZWJgnQaPIA1RpwkDDMD7GtgPAhz2w247d7AMziN8w7h3btlFaUa1WObX0NQAGP9HgF/53/xa/8d//Bp944ilOnjhOrSY5MT1Fc36B2fffpz5Uxh8q49kV6tPTtFZXicKQdrvNiSNH82netA2k68Ge53mkG+rvrWX/Wpa14XuecerwNJem5kgDie0XqI1U6KVNWt2EJE45Nj3NzKXz2BpIMoRrE/R6NFdXGR6b4Ks//Qb/02+8yOyFl3j3zW8z1/hDXnrpy0xOHUMKwfThw8xeu06tVqM5e41PffJTuOcKfHD5IlEYIOQW8zQahmFscA/HAJqAMWf2w71z55kXDsSu382N+CBs9wGlb9uhcRLjuS4zM1f5+ULe/Xvsiwnz12d56rGzpFlG0fdRSYfzb77O0gcXePzYEaqlAuXBAQbGRmhcvspqo0Ecx/iFAocOH0baNlJn0J9TV1oWrpRYlnWzTEu/BTAPAPPtkWh8qXj6sTOsLkfguHiOpF4bZmqqQpoqiq7DarFE1G0jHJuCX0BpWGk0GG21OXzsfSYOnWBuZpLLb/0EjeTvYdsOn/0xmJw8TLlc5bHHz5JpyUq7xfd/+AOiLMV1XcI47u8hEwEahnFn2w8A9W5ixQf1TmaB3svhkVuVYLmTbBev2Y21Ujmb0RyMUitb0BabD2PVILL93ppN7CYTeOvt3k4L9MPf4qzBzZMzhMpLrfie5sVPvkTlO2fQwNjJ1whX2qAylls9FjsrDMgFBlTC1LCHjJuk7YSyN0rabdNemkMnIVooBkaHGTtxHGtwiLS1jCsFmRTrrX1Sayyl0GGYZ2hbFlg2aRKTaXAsm7KjOfNYlbmF69xYXsUrerhFn14KrVaLG50OYSbALdJMNEk7wk00IsjozjcYGfX48kvf53/4jV8kXP4SY+qPmbt8gXfsl8laDU6cOc30VJVQHMMd9GkETf7w29+l1w4QOq8zmKrowb38GoaxL3YQ1T3IJVB2Qtz2+13SH/nDDl64X0HMVsf2IARRW7nTcdrPfXcnuzmHBJudKzsZfvBQB4EiLw0ktKLoWpyZnuRPfe3HOal/gXPfllTGlyiWLoIuIYWgF3YQrkcQLSHDLr5dJM0SQhURLS3h9FwGLAtVH+KJU6cJEkXQC0HY+OUKMuwC3Az2AKk1nrCQ/RZBrTVJphDIfMwgCkXG6ceOYF+xsQslpO0RC49OEObTwKUZQkiSJCHshAyEEb7wiNoB/qjkyOQiR0++xeUPn2LurZ+jfvw/Ys65RLhyg5WZDzh68iRHnnqeycNTBEGP+bkbLM4vkKSQZul9PECGYTwodhAAPkqPk3v5WdduxjtZ537ewA9ymZWPc5C33ZQguie0QCqf4UqBn//SC/zZn/wio47i5b83BMDU05ewhSQJQ3xngKgbYCsoF0sk7VVKJZ/RwSG0EOgkpbvcI1ttkzQ7DOAwVhvg6jsfIHoZJ4+MIfuHcW0c4BrbcfKg0LZJogi7PzZQSI0tBXGaMjY2RqItmp2AMEmp1WrUajU+OH+eKIwolYr0ej10mNHt1uhpi263S6vZYmBqlEMn/ylXL50l7jyBk3yO4co1RBYzf/UKSa/LtHYZOXyMn/jii1Qsm6jT4YevvUE3Ugfi8ccwjINtH+oAGrsrj7PVDBNbu318lGE8LLTWCClwpM1jx07y7/6lP88nj47iNa9TClZZef8UAMeemaHouCy1A5qrK3iWTWu1xaVrH3JmtA5JSnu5QbFaoddss7q6gmq06fRCmo0WK2KVbidATimSMAJHk2X5lHAbWbadJ1v0i3TL/kwgWRaTAUpp0iTBcRx6wTLtbsi7F66xstri/fc/YHh4iHK5RJqmdMMOcRyjfE0QBHS7Xfwo4uzjA1y/+j0uvPslWtf/ArVn/wtsJGG3TePaFZYXGhw99Rjl4VHOHprk//If/nv83//f/zl/8L0/QQqJ0trMJmMYxpZMAHiPCSF2EQD2p2e707xqm1BamYv9I26tNArkQdPDcD5ordezbT/7/HP8x3/tr1LRMXplDjcLiFeGaK3UseyU6TMzIByyMCZsJ8hMMjkyRmWgQ29xjqRkMTp1FM/3We61iIKQwcEBxo8cZbQbcvnaHM0oJswS4jTB9z0gLwWTpje7Vjd2yat+WZi1YCsKItJMkWZ6fY7hMM4YqA5w8fJVfM/F9zyKxSIrKyt0u10ajQZnJg/T6/UIgoA0TSmVyjz2+Le5+uGnaDWHuXzuk5w5/i18lWIDRQFyeZEgCum1GoxPT/Nv/qVf5dzFC1y6sQJao5QyFRwMw9jUozKwzzCMB5SUEqUV04em+fVf/xucnCpgxQsUvJjKUJlrl54EYPrMLKWizhMx/AJnT53hk09/gsMTU8TdAFdIpscnSTo95mdm6ay2GB4dYeLMSQZPHmHqiVPUjx+iLTOuNhboxhFJkgDQ7XZv2aa1eoBrJWG22u4wDFlZWSFNU44dP87nPvc5nnrqKYaGhigUCv3agorLl68BebCbpAlBEOA4DsM1weNnfg+AN97+GkQ2BSmouQ5HB6oMS6C5Qnf+Oo0bM3zuhef4K//2X8rL2PSDZsMwjM3cXQCo73Ix7mxtYs/tLns+TlPcYdnN2kxLxLbs9Ljv2/mwPzQajer/ngdFriX5ma99hcePTrD4wevUfEWt4uGVClx9N+/+PfLMDK60sRFEvYCX//iPWF1e5NKHHzAxPkGhUGKgXqdcrTI2PsrxE8cYm56CkkfmWYiyz8SpY3zicz9G4lr84I1XeOOdNwmTENdz8qnh0Aj6Xb1ZTJYlaJWB0nmDvRJoYaGxiFPN3PwSGovhkTFUmtDrtEiiEPqt9ZZloRGsNrustlr45SLdKCAIA2wBvuNwfPo7VCs3iOIyf3j1l3lr8jRBuYYlwBbgWQKSEE+nWN0WP/0TX+WJs2eQUtw8FbRAaGGuu4ZhrLvrFsC1IiI7WR7M29J+swBnZ4ve66d9ST5K4PZld0dR9MtpbLYYa3b6bbrDoq073vDXui0PYhehFhnaUmgpQQpGijY//enTdN76LmOZxYjyma1P8uunP8mFdw4BMPTcIq7l0mu2IAmZGq3jiIjD04M0ewGBElyevU5ChlW0UW5GmHbzzNxej24UU6xUmD56jPHpad6/fpnff/nb/K9//C20A5IUEUfYWUKatcl0F3SE1hk6zdCxRikfWRglkVUWGzFYZbohJKmFDlt0FmdIew0ckZHEUZ7VLG3COOWtCxdxRgdZitu0wyZxe4VaxaM+WuLx5/8lAFff/Qz/xeN/jr/yM/8u35o4TqfoQ9HHd2ysdge/0+P4yCg//+NfwXMtsAVaSIS28bSDtaflrQzDeJDt2dXgTm1Ft7cbHbzbzUGz3b15L/fsdt5v5+vcGHQc1ODj/tnNcd/9cTrI+1+g+0W+83bj6clxjgzWCVeWIFW8WyzxHz5+irn3RpCBRTyS8J/+mcM0VEar3cLzPQbqNWauXWV+aYHRsTF6QcD8wgJpmqJFXkQ67vXQq22ypVXi+WXiG0sUowy10uLq5RscOv4EleFDLPUSUr9K7JUJpEsmPDJZIPVKrAqLmTThe5cv8t1LF/jRzFVevXKR9+Yu4g/ZHD05RGVQkRUTKtM1qkfqFCbKlA5VqB6rM/rYGIefOUxat4jrAu/oAN1KSqsUk4xYyMNl3vq1IaInW1iJ4OT/WCW2LP7m018h9Ar4nk/R8VBhRLS8gm40+eKPfYZD4+P4rnvLXjUMw1hjkkAMwzhQ1nKfdP8/tpScOnoCnWaoJCMj4xvTYwDU/3UZgKEvX+bJ8uv86EWNE9fz8uWWxXQ2iZCSyIKxn3oOKQRv2BZCKrQQ/ZgoRZMCEVr0QID87DB//n//V/t1B+ECGRdYuG1LUyBc/z+bSez+33qUmGBiw7+CZIxDnOLQNvfDan+BOj/NZRr/Xoc/+Ld/iYnaCpeJ6dg+l5wyn0hWcC2bbrdLe7mBU1rh+OgkZ4+eYGZ+Md+XAjJteoANw7jpngaAUmzewJitP933f26LLsCHJYtxv+QZxztr1NUcjH288RxQ6gDPPmLsC6nza4QQAhs4MjlFt9HEUxqtNF3LQgFHfv0HeD9VZbi4xJDogQ+Zv6Fe34bZWCzy1rDt1cjbfWuZVv1IS+fjAgUgtEZpyNKMNM1YuwSqLCPL1PrrbEviex5ZkmJbFpa0sKRk1i2THov47G//MxjPWJk5zivBFE6zSZpEaKXQcUJnZRXHW6Z6eJxPP/403/6THyC0WJ/TR5tGQMMw+u55C+DtXUz5U/32Zjo4CIHJg0TAeovFtun7Xz9w47E3x9yAvAvYIm8N9CybsaERLKVxhIVj2zzZbPPq8CCyrAi+2mF1OeIP0mf5P5w/j7+4jFcfZejU45y/cJlz5z5gIA2QrSad2Tm++PzzeCWH1NbYSlHqxQgtUVhkWCBdhOWSChstbLpRyOzCPCutJrWhOijF3Lvv0Gp1WO2FFAeHKQ4MMbuwSK1YZsCSFB2LsguHRmpUfYs47DIfC773ozc4d/4CiZJk0qHRbKNtibIVBc9moOTztS9/kbDTxLclx48dZ6g+zI8KLr/72FP89fHvcYM6yhY8M3eZQ41FMsfJH5pSRZKFBI0m1eGYulNAJhlC58GfKQ5tGMZGO5gL+B68ez7E587r3+77mifbB8PGY74f77VT5jw6EGT/QAigUigxPT6Ba1l4jovne/zS7DwXqhXSet7C5yQpP3+lwekbAbONFCkFzUsN5s4t8Op33+alx4+hlkLiay2sYxFl4RPIBKES3DhFI8lEnuOLpZBaUYgDbC2oOZKh0WHiyWEySxCEXZiocGh6ENvxqZTqWJbH9aJPrVxA6IgwCHEdB2k7RNKmnSVcuXaVuevXUUrhuD5RmBCGIcWBKq4vGR8dYnCgzGqjwdHDUyxev0a73WagWucnFi5wWMxTGMpbyp9pXuUnv/8jUBlJptBKQZaRZhm9Zgvdi5Bxlhez7u9Ixd2N4DUM4+Gy7QBw40VjY4uNAOQOb7QSsd4VkXePsOlVaePP3Unejbn5Oh4tYheZwGudQzsl2TrC2nx9cosxSOtVgdbOibtuEVzLTd8p00ZyUKQ4CCHwUAx4MFJycFWEJ8C2BTYpf+Pc+/w/H5sE4HPL85yY6xCqFCfT9GYXGSyNk/QyLKvA6NhhGp2IJIUsy89FG1AIUssFBEpIQPYvaClCKoTSSAS+kDgIUqWR6Hze4F6A5/pIJclixeDRCXQWIaVNRpUwzciwaXYDLt9Y5NV3znFtcQnLcRkqF6mNlHB9l0xrtIiZHhtmZHCQou/QarZRSNIkJQm7FDyXTwUzLMb55/1E9xpeGqL0zWEcWmmyLCUIOqQkpDrG8WyIYoB+CRvDMIzctgPAjSPLLMQtXY076XVc66a0b78UbXqfF9taeaZVPq7wkba2n3YY+Ojdjrfbaqzh5uu75UHhtkOlgbS/utszU5Xa5ewmuymJI9RHN87Yd1oIlHBAQEHHTA6WGLBSRNDF8S3QGUpBUHJBCITWFHVIplziKMBNgUQw8/ZF2osdpqZPMrvQpFaoUamOEEcZZODZFonQaGnl5ycCS/SvOVqhbUm6NqsKGp1mWEBJuGSZwvXyy6dCYbv5+aYsgRASKSxSAZ1OwFJjmavXrzG7tEwsLBwh6UUhbsHn2LFDXL1yBUdC1uuSFgsIz6Xd7OC6HkEQEHRaVAaHcEtQiPNgLhtw81I5/fNVa02qU1KdIaUitjKEb1GuFFlKQiwpcYVNmiVk5hQ3DINdtgBu9v87sZdPoeZattF+Pd/v7n22etXeH8PdbJ85kw6qifFxbDt/ZBTrmbvQ8fOnBj9LULYiSzKSOCPRgkAr3vvgfcpHT+G4HkfGSmTzc1wLAuJ+EAUbz5SP/kmj1x9GBALbvnm5FNnN1mIlbj6kSGmjVEYQRay2uswvr9Jq94jj+JYp+rIsI0kSer0ehw8fIuy2aK62saRDbWCQNNUEQYe4q6iXPLqeT2Va4if5tsdl95ZZPpIkIY5jsizD9TRxGDI6Nkqh4GNZFnGSEROTTzF5d8fDMIyHw8NTFdRc1AzjoSL6A9ZKpSJxHOO4Lkkcr1cH6Hh5AORnCZmj8u7SWBNLG8oV/KFhzl+8wtJCgziKUVrhuk5/7f1uU/KctLV16v78uWuZ6GuBnRC3FjG3LGt9Wft/KSVaKaIwpNNu0263UVphWRLHdfH9fjAWxywuLnLhwkVu3Jin1WzjeyVcp0Bztcu1azfQyiLoxQgkYRDS7XaxfLXeApiUXaz++67NQZymKVmWEQYhQa/H5MQkfqHA8PAQjuuYS6RhGLfYfhfwFiVdDgIhBPK2bkPDuFcexpI1WxWEvm9Z2ZZEoJFSMjkxAUCaJrhSkCQJSima/VjOTxMQ0O51iUKJKnn0LJv6ocMMihXCKGNpcZknp8a4ZjusrjbIshHSOEIJjUgzNpum0HGc9fl0b98/t4yD7v95rUUvDAOSNM2DQpEhhCSKIjKVoZQi67ceFgo+vu+RpilhL+XQoaM4jsPy8jLeYJnmahfbdml3OhQ8G2mXKcT53MRxySYVIJUiDEOCIMCyLDzPI9aCTqdDdeIYaFhZWUYpkWf8myjQMIy+7XcBH9AZA4B8HBDkORCmjIixD9ZaXR4Gd/pu37/PmI/Dsy0bx3HJsowszcgsBVkegHfcfLsLSR4UdZMeUdfFqxc4dPosT06fRn7nNa5fvs6li69QJ0ZKSZKkpGlKohLiLMWT3qYBoJRyfUzq7VMWbtxna616YRjms4z005qEFEjLIoojoii6ZV9KKfE8j9HRUdI4o+xVsKRLvTbE1NRhbszdQGDRaXfJ3ITDhwcBcIIUlAYp6DkavdQmiqL1YBVAp5oojhh0HCYnJzg/P0fSW0sEMYMdDMPI3bs6gHdzldlprLmWAbxFksGBcGDj57XCEHu10+7igx6E43anbdjw0R6W4O92B+1z2baN7dhkWUaapaTo9Yyhdr/gsx/mwU0n7WLjoS2L1SDgO7/7+3zvD39ExS5y6nCNb3/3e5yq1eiGMd0gxCmJ9fF48NEzV942XeF60HfbD6ZZShiFKJVh2RZJ2v8HrYnjiDiKsKTIS9v0h+CtB4AjIywvNSgUihQKJTrdHnGaISwHr1AmjBYol2wyGQJFki64nZi46tFxNXYU5fvItlFK5YFtJpBhRBqnVCoDeF6BbpDk5RaEuO91P++Hzc7rA92oYRj74J4WgrZ2kYmphNrxBUpya21pq/+krjWoXWe57p37vwV3IpC4W/zTzfKx258xRLD1aZWxWYQlAHuLnZQJgZKbX6j3vPtVb7Hd4tYyug9St6+QdyrXs7kDEQTqDdmtabqe4KBF/rBiWRY9Pz9eXi+fcC3zIuwsIEoi/qe//4+YW0546rFnGKkNkBSafPHf+MtUEov2zAxLKqWaxHiOIFMxQmukBqnyKgcCQRymqP5sJGvH3LIshBRgp3mXbpYRZCGJiMnsjDRJSW0brRRx2CMMmui0jUNA2G1jY+G4PgATI6P02h3isEfiuwSqh1cs4ddr1EsV0utzRCHgp+hC/v6tZoBs9qDqEZYsKuJmcsradsbdLl4xQLUzjk8+Thq+RtEfoJMsb9rS+ajQOh9ScCDOb8M4AO5pACj6v7ZrN0+m4rbfN/7pIDzp3v8tuLP8KXizQL1fnW/tKXnbM4ZsVWr29mba7b5CbFoJaO8v4nfYCi3uqmHzflnfbw9wS4fSil6vR5pleTewFEhlobWmW+gXgW4FUAWrbKNDhW3buLZPwbN54uxZsqBNz0qp14YZKQ7jOWUWLr+NN1QljVeoCgfZn3nEEiAzjc4UGkXcTzqxbXu9xUiKPNkjUyoPAnXerKcBZVkoLeklIc1OQKwUSZb/bH2gRiacmy1PSjM6PEK1UqbRXqUXdvCrFQaG6sQZVIaGaM/cwC2Wqdby8z3oZohmDw7VsUYqFAqd9cBvrbVSoFFJgqUlJa9CtVTn2sKVvLrNvlZiP1iklDiOk59Hman5aRj3fCo4wzCMHZMS0c/KbbfbZGne4pYJgUwz0jSl228BdJt5AKhdiW0VSRLNn/sLf4E//O6PkFaK4wt6C6s0Ll6ldrTM+QuXKVWrzGYBRSWpWy621lhKYEtQaUqaRGApoigijuP1DF4A2X9gypQmy0AruV53MtPQiWPmVlpcv75IpvIkELdcZ8yRhHGeCCKlJMsyzp8/j+PaeEWfkZERMqDZbILlUiwWSVJFrTZEpdoEoNtModEDICl762MTN86bLqUkTfO5hDOVB8T1wTqrjTAPfB7N+G89w9u0ABpG7qEPAPd0nMdtrWCCLVqGDsD8uo8CM4fwQ6wf0GRpRqvdRilFkqRo20ZplU95VszTgEvdFLDAkywtdjlz9gRDJ0/S+Ma/RKqQuLXMaNll9sKHfHhuBgoldHWQp59+Eqt1g96FixQtB2lJ0lSBnRHHgjBuEmZ597Lod7U6joNOU5SVd69nqSaMUrI0RUhJmMGNZsDscodmqKkO1AiCAO04+JZE2ul6S53v+yilKJWLJDrFcRxKxTJzN24wfeQ4Fy9cZGW5wZXLks88nyeBtFZS5Go+vVtUctD6Zq3CtZatLFM4AqS0WF5eorHawC70x/49osHfmjRNP/6HDOMR8VAHgHl3yN5d8W4fB3f7rBVb/Zyx9/ZsxhDjYMoylIBMZbRbbWzbJunFJJZGepKe1GR2Pta3Fsn8ccuTCO0zN7uAM9jk8OFJjh8Z59wbLzN9ZJLmSoQT+7x78Rre6DCrHRgsDNHzF2gFIQNFH2lppK1JhU3a7ZFGbbTWRP1kC601mVIIx8OyBSqTZKlASo8wDJhv9lhsR2i/Smm4hF8u4Q5oHNsmbDYR/XX1ej2EEJRKJSxLkvXP31KpzPJ7H9KLUoZGJ1iYcxkarGJZAqU0vZbGauZZz9lA3ip5e/3CLFPYtoNT8FldbdLpdEm7IUI8ut8Pk/BhGB/1UAeAhmE8+FaWV7AsizRNSVOBldn0vDz4c8IE3QqBAk7F5cbcMt979/f4qlcjCLu02is8/uQp2u0GuugQhBmVgSHeffM8rW6bL7zwNLJeozjm8qNXX6dWKhO0O4hMcbRi98vG5LNsrAWASoMlXOK4X1YmVgihabUCVls9YuEyOj1OGIaUy2UKhQKL8/MUM4Vn5eur1WoIIRgYGKBcLXPl2lV6vR4nhwZ57vnnOH/pKiPDw8RHjlOp5AkgQReSRGM186zntOptur8sy8KSEuHkNQ8P/khkwzDuh+0HgPtWQXTDYPxtt+jomy89YD6SWqD36HK8L59VbDjutydw7PZT7GxCOEFeveLjSG7Ntt7bW14+U+zmK1UH8rx78OXJNwqL+dUWQZanKklboLWi2+/+ddsRC1cWGX3xMF7Vw6sN89UXnsUvFahVyjQbDZpLPcYPT4Bn4UYJz596gouXL3N15hJx7wxaekinyFMvfplrV67x/uXXWJy7QXxihNGiTy9MkWnGoGdhhZpMa4Koi18oIbUFTpXZa3O8d+5DlqOEibOP45TrhFmTdpAwNDrBpFugoEPmrl0lIcMSFkop/HKR+tAQS80WczcWSGJFqVjmC59/kURpapUSInkHSAm6Aq3A6QeAScW7pQt4rSVQSIm0bWKVsbCyhNIaLbUpAmgYxi120AK4w5IuGqSw1m/ealvFUARS2P1SD2tdqdt5nWK/iq1sd0aUjT+3sYqJRudZg7uk2c+yMpL12QK1vvlnNJCy87vJVufQ2vo22QKdLx/vZqitgHhP95K4Q4mYlINe6Gen7vfYSgFIrck0xNJnMRI0UknNslAyw7Jcgn4A6LUjukt5l6hTcrCmRpg6fZLf/cY3eOaZZ2isWDRWFSP14ziuR7mwzMhIgcNHnubatTpZHFIu1lld7iHslDc/WOIPXv6QOMn4YHmZY4eGSLoBqhfxzJkBPK1pNBt0Sppf+KWvcOXyDUrFQV5+t8GsmmS1t8BEaYjMKVMfq9BaWWBxcYmx0UEKvs2R6kniKGZ5ZZmgF6AKFoltUa5PIZcjrs+tcPjwYXrNDguLi0wdPYrl5t+7oGshNfitAABVdIh0hqvlevevUook07jlAimKVqdLJjRqrTg15nnFMIzcDgLAnV821idvhx3ECuKWJ9qPf9/9u0FtdxzJx82s8GBcgDfbyrvZ8ju9duvWv92tfy/PiZ1v94Ps9nP3/o2rzL/7GoFSECcJrucBeQJEUMprV3qdmLiToFKFtCWvvfkyzdUOrWaT3/vd38XzPL729a9j2y6vvPIKQ0NDtFpNlFK4rovnurTbHYIg4k9++EccP/M4n//8F/jO975PZWSS2Wabil/n8o0PwVtibHCYi5fnkRX4/p+8Tq02wo2Fa1y9PsfwyDhjE0MUikUmJiZprSxy4sQJ4l4Hz7NoNBZQKqFUKjE4OEpUDBFS0Asi5ucXqA8O0ul0SNMUz/OQQrCyssLpo/0AsJPvGRmkiDhDuxZp1cNtJbfMTew4Fpa0UGmaj429D0fPMIyDb9/GAObVqcylyHTDGA+SgzDlncoUSZLgVTyE6geA/RZAu9kjjmPiZow/5ONXJEvLyzz22GOceeoplufnWVpYoBcFtNttlpeXefbZZ2m32/i+z7Vr1/C9EiurLZ7+xCcoDQySKMknP/VJWklMtOqjCyWcwZgfXZrnSOJSHj7C5Q9eY3X12xw+dATPKzA6OkIUBrSaLZ584iiXL1+m5NnEnkUQBERhhoWF6zj0unG/rqCD67hYlovv+wwODtFqtZibm+MTn/gEtm3z/qVLVMv5/l9e7Gcka3BaEfFwkaTiQitBCHEzANQO0soDQN1v9TOXHMMwbre9/sw72DhP5sbFsm5dtUAgP+aXJeQt7S23r/thIBBIIdcXwzjo1gsM71MmZZ5oofKrghAo8lp2vu/junniw1oLoLWSF0IOVvNu0UrN4/MvvsjRo0dZmpvjzddfZ2RkBNuymZ6e5uTJk6ysrCCl5MqVK1QqFWzbYWJykjiOef/cOY4cOYQQkkOTR3CtIu12xPDkEQJtsRBEhLbH8eNPE4c277xzgXPnPqTVatHttXE9h0ZjlanpacrlMsvLK7iuS6fTZaA2xOTUUa5fX0QKFyldPLfEQHWQpaUlzp07x5tvvkmaprTb+Ry/vU4Lx86D3lYzRal8ZhSr/3nDkn1zKru1INC2cF2XJElMwWPDMLa0Jy2Am94Y9DZ+ZrOXbPix+z0W6V54GD+T8fC6n+frxplMfN/HcTSWlmgBQTEPAEthXhOQvHEMrwhXLl1iaWmJ4eFhnn/+eZaWFllptRgZG6VWq5Gmec09rTWXL1/m0PRRBkolOr0IaVm89945fv7nfo4/+Fd/wMnxUTphwFsfvEel4lKquMwtzjGmq0hKnDpxnAsX3ydJEo4enWawXuXw1Dj1ep1rq8v4vk+j0aBSqVCpDtJtdzhx7DR+wafb6ZLEmtjJGBoaZmRkhGaziWVZfPjhh5x57DHOnJlEiPMkiSDsKWSSEGuNbPSAIdJ+IshaMoht2+j+bCNK6/7Y3Qdj0IlhGPtr+01Qeotlq3/brZ2+z+2JqTtZDrAHcJMfXTs97/b7QB707dvC2mZYQlIoFHBdZ70nICjlXcBipY3tOHSWugCcfvwYQkqKxSIDAwNIKRkaGubY8WMMDQ7SbDbJsox2u81AtUq1WuWVV15hdnaW1157jU67jdMvnzJUcLA6DWRvlaOjNb702Wf5T379/8gv/PzXabdChoemeP75F6hWaxw9eoRi0SMIenS7XVrNJpVqhcmpKQYHBxkaHOSt196iXhtmfHyKOMqIohTb9kiTjG63S7vdZnJyklqtls97rBSTE6X88wb5VHRRFBGFEazkn3etFMxaAOi6Lo7jbLEnDcMwbtp2C6C9RQue0Dboj8aRAnZ13ZFsVf1FI0k3qQkiUBrUbh5y9f0v4bFVy6jSB2/E5M2i2oJ85tTNtnA3ecpbzUe82/XdtvYN+1iydVmZdDd7XFvseCSFUPt47u0wex/I9/cWiTm3na8bv+dr3cR33VIoQAsFIsXWFmUshnwfXyREWUpmOestgNlylzBOsRp5N+ngcJGW5RAjufj+eRwki0uLZFIwffgQJc9ntdHE81y8ssPJ48d57LHH+Rff+AZPP/MMYZjwyqt/TBqsMHt1lsWlBt04JrMsVsPrCGXj2GWaOqLuwdzqPONTI0xPjdJqLHD2sVMMVCs0lpfQWUpzZZnB2gCFSo3pYxYXL88wODSEFjZIB9cv0VpdpuRoPKlwLc1yu0WmNSutDoNjeYZzt+eAXSJWNr00wV3OmzyzagEp84BPyrzr13IctE6xXYm01jLss/zEF+xjSS/DMA6ybQeA1lY3Ob2hVMgeWLtGbfJGbFJVb/1f1v952+5/eLXVTCX5lh2smS02bqvW+g4B4G5L8mx1Dt19iZWN49cstfk7aTTZrtKUdlNYYz+P697d7Dd7WNksAIQ96C6WCo3CUhlFIag4DpZSoDMSyycu9IOeVkyz00Ou5v/f7i3z8vcvc/bsWY5MHyIJI8ZHRhGuQ8H1sG2b8ZERWq0WizfmaXU7jE5O8NRTj6PTiKmxIZ7+C7/C9dlZXn75R2SywMzCEom2iJXkb//tv0OqNQOTw1CSCF/yyeef4ch4HTFRw9IaG8XTTz3Je+++C7hkWnLu/CWCZpvhwSESpdGWTaFcQVv51HYjgzVWGg063Q69WLHaCfDKbY4nAXjQbNmMjR9i9do1UjR2o18MesBHkBeolsJGCgfH9bEcgSXBskGT5Q8dhmEYG2w7APz428jd32juvIbNbyg3//Yg34TvwoF+WN+rjdtZceh78Y67e6ddNTvv6p1256Bv3/Y4joPtuuhAI4Qk6mcAi0xh92KiKKS7nP9doWLx0ksvIa285TMKQyzLotPr4RcLKKXodrvEcUyj0eDcB+9z+PgxyuUylUqFV199lXanw4tf+AK/9mu/xvd/+CaFcoVGO8ArDfDMp57n3AfnWWosUq1UsSxJmqa0Wk2C1WVmLl/i6aefRsq823p4eJj5+XkW5ucpuT7lchm0RloWcZLw4fnzFBxJrVzl2o1Fuq2AwbFJhkt1ojQjS5YAaLUljcYqtYEaWXsFlte6gN2P7K88EJcIAcVicT8OkWEYDyAzFdwBJdb/czAd9LI+pvTFw6PgF7AtCw1IKYnK+bg3txPR6/byjNdOnu3qFDQv//BlbNumWqly6NAhLl+6zOW5a3zimWcoFotorVlYWKBYLHL06DEWFhbodDrUajVOnTrFzMwMly5cZOLIGaanpylVB2j1YgrlGteuXaPT7VAul3jyqadoLl7j2swsA6cOUygWefbZZ1FKMTc3R7lcJojyrunjx08QtNosLS1RqVTQWrO6usqVK1c4fvQQq52Y0cnDNLsBr735Dp0w5plPPU+5lHcBLy0ldNs9jkyMMx+0UEs3xwCu7Rfol+3pD+S0LYtarY6UEpMLbBjG7UwAeEAJKbAQ60GMUgerC0ewoUu4/+ugECKfieXgbJFxN+qDg1j0ZwiRkrjsA3kRaNd1cByXLMyPtnASTp08ycpKg09+6lOsNhrU6nU+NTFGphU3btzIawp6HpcvX8bxPC7NXGHm2jVc16Ver/eLJ2u++93vUqqNoaXN8vIyzSuzXL42S6vTxXItHj99EpUkJGlCo7HCqSPT1AeqtJtNvvvd7/LZF16g2+vxxhtvcOjQITqtNkP1OufPn8e2bRzHwbIsVpstKkWXOA25fG2OQ8dOcGlmjpXVJXwXlNL86NXzHK4NsKwjqtUK3dUmANq1UL6Fm8n1bndLWhQKBaRlMzI8gu04ZEl0oB8oDcPYfyYAPLDyq7UQB69czMGZLWIrN/ed8eArlYrYlk2m8nGxvQ3zABcKRYaGBLNzCwA4nkW1XgYEK8vLdLtdbszPs9JuMjg8xODgIG+++SbHjx9ndnaW0Ylxer2Axx9/nIGBAQqFvJtYa83U5CQ/fOMcw6PjLC0t0YszpLSoDw4yvzhHGIZYWUaxWGJ4eISx8THefPVVpBC88MILRFHEwvwNhoaGeO/d9/D6s3MkScL8/Px6tm9jtcns9Q61wSGe/+znuT6/TC1QFIs9ABrNjLm5RUY9j4s3Zjg+NoQvbYJujC65JBUXf/XmVIrSsvqlYRSlkukCNgxjc9sPAO+YObZfd1qxab+eWOvw22kckkdXu9qMfbNVaY4DG9ysJUXcx6BwLSFok03Qt/2+2b/dc1r0z70ttuDAHtv91N8JQlMsFrBsSaoUWkO4Ng9wJyIMApIkRWhBEmY4vsWf/PBbRG2bz7/4RcqVAaJrs1TrgzieT6vToVavo7XmzJnTJEpRqdcZn5hgaGi4H/hNcf36dQ6dOM7I1FF+8KPX8As+R08cZnh0nA8vXmJsYpgo6FF1BUGrw6UPLuIkCeNj47TaTUqVMsvLyzRWGliW5OzZx0h6IS//4AccPnSI6elpVpur/ZY6iV+pEIQJV67N8f75i1ybvcHpowFwnFZL4kqLlZUVRgou12ZnOTw+ilwNyfoBIM0UEGgB0pJ5IW2tqRRL/bm0hTmvDMO4xQ5aAG+vLZXLBxt//JXl7rswxZbbYJEhRf8JWIPaRuaoQqC0tYvM4f0ZTSM3BAcasDbMhJIc6O5gcYdAKmMvsno/fnvA3uJtlIRkq2O+b7tVblo6CTSIFDN6UQA+Umu0TKgNegiZEkcprvDWp4FzOzFJmtLtdrCkRdxOcXwLrVZZWQa3XOWVN95jYPQIdsEm6HXI4oiZuXnmbszx/Cc/wezMDXSxwo+9+CVmr17jxrXrhJ0u7WaTsaEWh8+cpnHmGMvdFs2VBb79jd/hF37uT3H89BhOnHDjjfeg0WHi0CGK42WmTh5iNXqTa/PXcHwbKRRFx6OxME+z1WRscpS5pRtk87N5cWvbpjgwzOmnP8e/+v3fI505x+yFD8l6Pc6+cBiAqCOh3UI7A3QzgaUlrSjDbvRgqko04CLmY9AaLQWpSEGHlIVmulrHSyDGIRN5ZrWJAw3DgB3VbxFbLLdOFbXVcvc2f/+1P0kEcsP/f/yy/Z/cXamP3bv9Xdc+m9zHbdiJW451f4s/uuzv/rvjFmxyaPcv5DoY59jBJ0EIpICBgQpaZ1jSwrYcwn4NQL+ToLUmjvMpz8LVPOGiPloijkL+4T/8DZYaqwwMjvDmO+f4g299h8tXrlEolhkcGubUqdMcOnQY23H58MJF5hcWKRSKuK6HzjRzl2c4PD7B44+dxpYCy5KcOnGCidFRBso+RUdyZGKcsydPkkUxtVqdpeUGo6PjdDo9lIIL5y+ycGOBlcUVtLSIlSJKU7As/FKZVq/H/PIyP3r9TRqtDq1WmzRJKHouJ44PArCyEDNUq5FEMUmSgJDEaQr96eDSipO3KIv84UtpBUKj0pRapYK1/oBmzjHDMG4yYwANwzh4pASl8F2PwX6XrWVbWIL1FkC/l9DVmjiOyTJB1M57AapDRY4erXJ+dpHZ2VnSl1/m8pUZlhZXqPhFjk5P0Ous8tprb6OkpFarceTwEYJ6hzSImE9SJiYmGS4N8K1/8Xuk1RJf/sIXkZaHlzm88oOXKWQun3niSWZ7CStXr1Oql8EXvPrm69TGK3zwwUVOHD7CxPgUQadLa7XJ0PQE3TBhbmGZoaEhmrPXOXb0GGEquHT5Mq1mEztJCKMIz7Wp1fJdMXe9y+joKFcufohnueBYJEmCaOQBYFK5tRTM2pjcNE3XZ0PZv9ZtwzAeFLsKADe26G23dU9KeUuywL1MHJDIj81KXeuwfNCsZUKufb673Y9a634LmMaSVj6U8i7XubG49d1nB+c1zdijz7vR2hyqe71e49brwp327VbXD60VCImQkoFaDaU1UloI1HoAqJeaJGlKFIWgbcJmXhy5MlRE6wghBM1mk89+4TjC9qhV6xyeHGd1aR6dJPzwB69x+smzDA4N8drrr1GwXY5MTjMyPMLVi5dYvHyNE6fPMHn2SUYeO0wvUVz54AZnn3icdnodR6U4UuF6kvpEHXeoxNTxI5w4fZwsgZLrs7rQpN1aYnFxldlmCxyLWEsuzlzHkhLLLXF55joLqxFnTh7HzTKmh+pYSUR9ML88Ly3GTEwc4tqVS8RRROZYeL6P6LcAJmUHpVQ+/69SpGlKoX+9LZfL2NLOx52qtcHEphXQMIy7CAB32q278TX3sqTJ2ntsNsPGRjqPdO7Zdtw7ot/bk3++LLv7MYmu6+K5Hu1Oe+0ddr91H5kx5C6DSQFS5l1XGwO2vWICv7233eDv9p+9hQZh5edSuVgiTRKE1mgBQSG/bIlGhzRNKJVKJDG0+rXxBoYKTE4OMnbsDDdWOvzBN79JqTLMc598jhNHDnHlwgeQhgiVkZAyPz/PkelD1EoVisUiU2PjHDt6lPN/8Cd0ZheRpxNWbqzQVYLZuTmmjxxiZalDe2UR1xbYrqQ6WGalu0Ir6HLl4gw3ri9S9ouMj00zPDBCFL5K9dA415cW0LJHqTrI9PQ0WiviS9d44skn+Kmf+HEuvfMu85cuUi9JLEuQJJoL569z9iefplQqoeIuWZahsgxW8izhtOrmM4HIvPTRWjAIYFkWUvQHj+hdJMoZhvHQeni6gHf7YLvjzOFdvGbtdXtJb/HnXW7DWqB2N8HfA8M0gny8O51T+7HvhEBIC8uSlMtl4jjGURlR0UJZ+dBlpx3heT4L88sIXESUb5hXtmm1WrTTLlZhANuyOX36MZJYszC/RLVSI+w28R2LselxVtIOYa/H6uoqMtO40qK1tEJzYYnTx07TWW7Q6HV46/JllpebjAzWSHpdKp5LmMUM1EtUBstELvzgRy/zzJlPcOjQUZbm5kkTheP4aCWw3QKPPfEUaM3yygqvvvYax48f48jxk0xOHabdatPrBUgpefKJaaBLc1XhF4oMjo5w+PBhZi6dBzRplkFjbQzgR2cDMQzD+DjbTgLZy4SOvU8WkYCVZ1ZucxFaYm1Irtjekr9OaLHpsl8D+z+SICLyZbdbEMcx7U4bKbeX0b2zbd3813ZpDUoJlBJofXt6xy62pz/XtNB5pvXG47u3SUsPsv1J5LnTfpYSLBRl16FeLEIag6VplfNnVidI0LGi0+4hhE0cpyxfbwLglSWkCSXPIYk6VEoOv/vNb3Bp8QrPfuF5UttFZ5KlK7Oo1RbdxUU+ePcdmr0W569doBm1qQxVKY8PcHHuAt3uMr4OefbENKcn6zRvXMLOEjorK4TdNqNDg4AiCLs8/9nn8QdKLLcajEyM4pd8tM44cuQwwvMIFKx0Ao6eOENtcJS33zvP4PAIrfYK5995m6SxipdoyoV8BpBGU2CNDhEWPAanx0lUglIxUqXopbzFPq24aKUQGiwhkBpEphBSEGYpqdDr86U/6me2YRg3bbsFUMrd3XA3s9VNdnddfGvrsXb4KoUtdvheWqO26NRU9HuUN73CZuxl38vGEjFS3DwuGoXaYv9t1VG8sctcq73tH9rYHZx/frm+f5RW2zjWa13Jt/7/TZsVSLzz2qTeuP9uhscaSDdEy3vRtf7g2uq7vvMySLsLqDVSaKwsY6joU3MdZJigbU2zmK/LbcfEUYYULpb0QUeoTn5g/YrNSLVK6no0ZmbxrIQznzrM+ZUP+ff/5v+NH//0F1l+/zxnyj6DmaIyNk6p6JJVPHBs3HqBd956l4nDNZq6SSdcwJpfYWhoiMOFlCRtkErN4uIiveVVRgaHmHnvPaY/cZZ2t8nSyird1RZxPMyzZ55gdT7i2PFpVsoVKBT5nd/6Bpbl8plPf44bs9dZWlog0z1KXplinFIQDmNDNhDz3pUGv//O6xx79gnGamVSS5OGPaKepDCX5OP6LElkg6c0trRQqYI4RVgWzahH1L/OibWRNyYKNAyD3TajHDg7L+kiEDdbg7a5rL3XR1uz7k/r32bv9PAVFtnb/bp/R+pBdTD2kFIKBAyUKlhKE8cRcRzRdtcCwIgszYiTmDjOl95S3iXqliwsIei1O4wODUOWcnR6kiToMDUxyvLKInEWUR8bZH7pBmGnh6UhS1K6nTavv/oqZBnDw0MgwHZsXNeh3W6RpQkrS0vMXrvG4OAgruvi+x5JkvLBufexhKDo+owNj2DbNiurDRKVEeuMazNXaDaWmZwYRQpFt9NkZKhOwbFw0pjl+eu0eqsUakWK1Tyj+f2LS8wvLHJ1ZoZipYJlWWg0WZqSxSminZe+iUoWWZaR9mcaydIMIQRJktwc3mHG/xmGscHDMwbwPjoowYNAbJl0sduhi48qIYRJELmPtNY3M4CVIo4ThEzo+v0u4FZIlq3VBrSwLIusm6GVRkhBueLSaPfodkNOHTnG6KFphken8PxhwmbI8WefpNBdJl1doeIXKA0MsqRC2kGPiYnDFLSkPjREvV5neHg4D7y0JkkS/GKB2cV5qtUqQT8oO3HqFC074+13z3Hy6Ekq5TJxFDM/O0dtoEqpVObsxBidIOS5Z55iYXaOxvwixeFhCHvYQcDk+BhJt4dXK1As593ZH1xcIo5jVhurFH0f27bzYvdKkWUZbiMgG/CJSzbZch4AZ1lKlmV5jcQkudmCflAuVIZhHAgHrgVQSrnlchDILX8djKurFPIjiyWlufjvkBBi/bwzYwL3n9YKy7KYnJzIgxmlkFIQlzwArGYvDxIFKK1J04Q0Skk6eRd1teZR9goMFEp40qZkSQZLPioNqQ1WKA4UiK2ESMVIpSnYLipKiHsBot+F+v7b72DbNlLK9coFYRgShiG+76N0HoStrDSYuXIZW0imxsbJOgEl20MnKfWRIbxqmcQGoVPiXptec4VucxmdRowN1nji+DEOVaqMVEuUB8sMHx3EL+RR2/uXFsiyjJlrMxQKBUZGR7AsC8/z8qB0Jc98DksWYRiSpvmUcGmWtyCurq6SJsl6lrA5lw3DWHOgWgDvdHE6CK0xW9YsW/vvfd7Erbfv/u+7B81OSpkY90IegB87eow0TZFWXteutzYNXCsEwLYdHKe/aIu4meBWbWxPMTRQx40TEiGouA7tVgffG6TRWKS1cJVC3ORovYalQWaaLIgoWA61coWKdPGBd95+k8HBwfUAcHBwEK9U5MMrl2iuNsmyjLGxUQrk7+PZDkVskm7A8NAQpaE6CYp20OPa1RlsMoI44MyJo0SdgKPTE7xx5RKTlQrStYgdD+HlQV23p+iFCUJAp9vGcRxUppD9LGghBLLRQwFJ1SXLMrIsQ0pBlmZYjkO318tnBjEMw7jNgQoA74u9uLdv7F/dbH130/+6lw/s97u0xy3bYDqlja0JS2C7NidOnCBN0zy5QUNYyANApz/2LcsysjQlTVMcBNFqTPlQAa9qk11KkAg8x+HD997j8BNP8u7l60SZ4MT0GNFKj3bQxRMupWqRoufhV0oIrUmiiFZzlYGBKkLkDwG2bdPr9Wi08lY1KQVJmqCFoFKuUD02xbuvvoUrLXzHpTY4yKHTJ0kt6ARdJIruapOkWubNV15janSMV37wfcaqJUbLPpeaCyhfkGZLACytxPR6PRCCbreLkJJ2u513c+d7aX02kLRyMwDUSNIsxXIcOt1OXlT7YHSgGIZxgNx1FvC9LOq83W3YaPuZxAJw7nqb8jcFiwwhsrWNQG0IbjK9my5YxV4GSDYS3d8GzYYZMPb0XT6OxfqdSGtuzk+18+zSm+vbzG7Xt7mNmaz3ohi18VG2l1KsCIbHy6Qz1/EyEKlNVMy7gN2VCAsbSytcoXHIiJKUqJWXT7EqEum7xGHE0OAgaWpz8b0LpEFCY6XBik548uwpwuVVuq0AEXUYHqjQjkKCqMvkseO0W4uoyKLXblAeqKJlRio12IqBgsNq2GXq5BSlkRLzzRVuvL1EwXdxhItVtKkND+AXXXBtChWf5eUKSdbGtWwcYgppRG91heJAmRtxiFUsUpMWNasFwIWZNitJSqohTjNipahVq4SdNq6w0ZlALa0FgD5JDFKA5VkESiH8DPyY1OmRqXw6OKHB9AIbhgG7rAO4N7X7duZO77+77dmq1tnOlzwXOK8reLO24FrlNLHh/7a77G3WpSCvE2j1l3tX2e3OW7Fer3E9ENz4592ub3/q1ZkagftL65SzZ44xUHJRWZyXNkkhLOVFj/1WgiMsPMvGd21818LzXcJWPvbNrzkoKbA8l1Rr4ihjpDbC2EANX2U8deo0BbtAvT6EdCRRFKDimJlLl3j1lR+xsLzA7LUrLM7fwBIQRgGKjFQnKDI6rSYrK0ukZCRkXLpyiQsfnCOOA5RQhFFAFPRIoxDQBK0W6IwsiVlZWuDZp54gCwOGSiVGhofoiIzZ+Xk6yy0sOgCcu9QgQZKhidKEKOjh2g62yK8zWgtYS/yo+Wgt+oskVYog7DAxOYiwFaw9nJrBwIZh9JkuYMMwDpyyV+BrL36epBtgI8myjERokv40cIVuRmLbeJ5HkiR5UkQmSftJINpNCYIezSDCr1TptTsszs+TZRknjh4j7oW0G02qBR+JoOQXkRrGhoap64x6pcrI8DBNpZhfmEe6DhOHp6E//k5pzdjYGFEU0263qQ3UmJm9RhLH4CjCbpeFuTkUGr9SotVu01lcpiwdYmmTRQnjExOEjRYXrlwiUAGDg3UKwmdiIm8Zf+/8PFrlNUTDMKTX6eJ5HlE/I1nAehewqnk3d55SkGS0G01OnzqNZzmkcZo3uJv4zzCMvj0JADd2jx0EB2EA/62zXRyM/XKQ3Lp/xAO1hw7C+fWw0TovymlJC0tK6p7Hc088TXexQVXapFrTLuT7XSYKL9Fo28ayLBzHwfM8siBFh/nPxPSQUpKlGVOTUyid0nZdJiYnOfXYY8xfv069XEGkKY7rU7Ad4iRjqDpAiuba5atcuXSZernE6MgofqWEVyzSDQOWlpYQYYxXKHDl6hW0Y1EoF/n8iy8StkNWF5fRAhzbIYpiUhSO57I0c5WVhQVKrk/cDTh97DhxN8CzCkTLDWqVYQgzBmoS0Jz7cCkfKiEFcRTRarXwfZ+e7eTt3ELeDACrHqlWuFrnfRIKkm7A9PQUA8USvW4zL230QH3TDMO4l+56aPBBK9VyEMp3CCE2lGExj9yb2biPxD53Rt+Ng3B+PYyEEFjSIk1TtFY89/iTHKrU8DOw+1P2dYr5mE+3HeHYNq7rrmcAu65LqVjEpwiAchJ6vR6lUjGvydcLCdtdZKq4cu4DSDJkphGpQqSK1nIDV1ioKKG5vILMFNOTU9TrdSzbIggjMqUIwhApJUmS4nkeR44cYXh4mCiOWV5awrVtkl5AGkQE7S5Rp0truUFzcRk3Voz6FYJGk26zxfz8PFoK3GKBOE5I4hjHTbFtTZZpZmZb+fRtQpBmGb1ej0KxQKFYwPNcHNfB6qWQ5i2GcdlCKYUtLWwNYbvD5OAQj586hSMknmWbXBDDMNaZLmDDMO67tcLbxWIRgeZP/+zPIaKMiuMjwoSOZ3HlsxP5z0qBXfIBqFQqJEnSL9QssJJ+mZiyZGVlEatYJggCKsUSUilcyyaJE4oDNTzbodVs4VsOKk3RaUoaRwyUK+g0Y6A6wOrKIs1OG6vgg5MH/YVCgYK2SJIE27IJgh4D1SqxylC9jJLnY7kuOkmxEIzUh+gFXaSSlMsDxK0OnswLOlfrAyw1G0xMjFPQDiUnn993fjEgSrNbcuWllBQLRbJqFRmGKEsgLYuoGaKHigTDRYpXuvnoWKXptTtIDV/98lf44SvvkMb6jsXiDcN4tGz/gVCLnS8G0E/p0Jsv3Glhq327t9v2gDS+7dDOpzMTcOfjcdsiuHPtyl1v95bfqT1+q03d8YTc0/fJf+X58lrnwwAylXD28ZM8++Tj0O7gWZK0ZPPNv/wYC0+MAhAMePzJ/+YJrKKHX/BwHBvbkvgFn4IsAeAWbeabS5SrJbqdVXzPwpbQbCwjyXAsgWNLgrBLlEXEWUw36FKulAGF5zq0200cx2F0dJTxkRFKhSKdZgutFEhBs9XMy8D0x+MlYYRtW0RhQBwGFFyH1tIyRBF2mqHCmPbKKp1mG0tIkihi4cY8vW6XxfkF2s0W5VI+hvHy1Wa+Z0Q/yUzkreSeV6BQKOE4Ho7jcO3Hn6VZrwHwyr//s7z34lniNEYLjSMl7flFXnrh8wzVBrBcCyEfyi+7YRi7sIMeAXsbi4OUXn9xkdLadEaP/ew6OwgzizhC4m6yOMg77E2Jra1Nlrvf7jwruN9FLfOZQu5rF764dQaTPVnhlnt26/XbCpwdLtaGLuE9Ob+0DdrZZNmq5M29kAHpJsveldbR/fdRMkEhUNpGIxFOytd+8jkG6VFzNK6neeMTZboDHnoteBHQmChy49NjpGmEZwsKlkSphETYoPJ9vypjBsdqjI96WE6MW4QwbeGVBIurs8wuXCIUAR0/xRopEnopuiiIRUQv6xJmMZbnYrsulrQgTpmoDzMxNEqz16E6VEcJsIRkZWGJguWwtDiPVZTEaRdXZIwUPOx2G6/VJUtielFIpVJhoFRGhQlly0V3I5Zm5xCpwvXy+oYXrzSIhQbbReBgOz5RlIG2SROBygQLpyZ56y9+hVjkHTmuVLz+yy9w8cwYaVEg0ojmhUscGRzh5LFDhFZCKkxRaMMwcjvoAv74oO2jcZ3Y9GX7NXD+IMwsIu643/QW/3qH7b7L7YHbkhjW/+5+Jszc3Ii92Yat9t/W697NI8l6Od49O8fvtBX73HKzD2+XN2rm+8lC4Dku40NVvv7jXyHrhRSkQFuSTtlGaM1MMIBGMOZ2KJLSLlmUk7g/bZzEtWxsx4FEgRehnYhOt0un02J5eRHb8bAdi17Qo1Qq5fMH6wzLtZGOhSNcllaWKJXyVsRCuUShVCaOYzIUSmuuXbuGlJJyfQDX80AI4jjGdRyaq6tUqxXiIJ+nuLGyTMHxyMKIoleg2WyitKZUKhFHMaVikU67Q6fVyuczlpLqQL7jL1xtoOl/V/u9AUJKXNfDkhYZsPzMcUSaEdk2XW2TASLNuHpqgqNz7+SPQV6Il2b8+I//ON956y20Vh9zTTIM41FhxgQbB4NJqHjkaADZP/Ra40ubLzz/GaYHR9FJiuvms1v4S10UghtxmdmoSqwttIRSI0SrvDC3JSWu6/YDwP44wGL+b37Bp1av56VkkgTbtul0OjQajQ0BFsRxnE831+uRJAl+qYhb9LE8FyUl2pKkWmF5DuVKmSNHjqyPWwzDkDiOCYOQJEmpVirYtkMURdi2g0bnn8XPxy4Wi0Vs26bb7ZKkKYVCEaU1lYF8Wy5cbeR/2PC1kP3PKPtlYCyVV3W+xCCvi2kWqIAQyCQljSKyKCYOQsJewOc+8zkqpdKBSNQzDONguC9XA3Fbt9n9cqfu4f3qNpZb/Lpf7kdXvRCSQqEAmLIqj5T+sExNPh62ZDt86TMv0L2xhI0kTVMsy+LMmyukiymptrBQVGTEyNU2k28uYNkWQkpsx8FzXVzHQcV5d7lfknz44Yesrq6C1utTpTWbeUkUpRQrjQa9oEe326VWq+F5HsVikSRJaHbaRCqlEwWkQpOgOHr6JGNTkyilmJ2dRdo2mVIMDw8jhGBkZIR6vYZf8PNMXcchCHq0W21UpsiyjCDIS7eMjIwwOjqa1/aLInzPoVTOd83V2TZCQJZmKK3RmSLshSiliOMIpRSTP/gAoXV/Vh3I5yPXHH75A8J2F5Eqok6P7tIKhyanGKnV0cp8vwzDyN23APB+z6qwk5lF7tW23vH97kM3zf2a8WKtdWTtz8YjYkNuji0kZdvj2dNPQLOHzBRKKYQQeErg/ChvEZtsr/L8t2b5/DcuYSOxLAvXcSmXy9TqNSzbRoX5yJaBIR8p83M5TTMKhQKlUmm9ZbFQKDA0OMjq6iqXLl0iim7OL1wql3F8j0a3w8SxI0Qq5cr1a1ydmyVIImauXePKlSvUBwaQQtBsNrEsi2armbc6+oW8NRKwbYd2u4Xt2Ni2TbVaRWtNFEWUy2Vc180XL59fOAgybiy213eT1hql1HqLpevkgeXAXIMX/6vfwV/tASAzxfP/+W9RvXwDUoWlwFKapBdQLVWYGp9Amu5fwzD6dpAFvItlO6/bbP0Pqn3bdrH747GVe530eae3VpogCjZsxoN8Ejwk9uq8+pi3WOv6l8DZE6epuwWcRGHpWx+BrhyZBODZ1y4z9eYCWZSQJAlxFOUBl+fhuh5+ocjSXBOA+nAe7PV6PYKgR5ZlFItFKpXKeitfr9ejXq8jpSQIAobHxhgcGgKtWVpZJohCgjjC8T3GpyYZm5zA9jx83yeOY2ZnZ2k2mxSLxXxcoeth23ltwjiOCYKAOIkZGx/nyJEj6+VuXNcliiK63S5ZlmLbNsViPo3dwkK4/h3IW8Q1SivmFxewLItSuUyhUMCyLIbOzfDi/+t/BkBkisE3L6HSDEuD1GBrQbuxims7TI1PmvDPMIx1204CsTZcOrbbTaeQ/Vond6DJCwGLtXXnUx8dVFvPAiEAa4ebrslnaN/kfW77y1uCIi22yjxAbrEBaosr/1qJmk23Ya13aT8SAnQ+N7Dszwqy9pYaRT6H1V5Zm0N4MyZDMpfPRLE9+tY/7uRc0RZoiRAKSwQ88dgEaes6RUKkyuv9CZURWXB9cgyAU5euk7R7kCnCTocsSZGIfJYzZVMsDfLBh99n/FMT1EdK6OIAqqVxHZ9CtUS73WFkYpy569dJ0xT+/+z9SZAkWXrnif3ee7rZ7rvHvuVSuVRmVgK1otAFoGUKzUaD0+RMd88IhcMLmxyyKRS2zMiIUGR4IA88UyjCwxz60iNDETaFw2my2TswBTQKVQAql8olMjMiMvbFw1fbF13eezyoqS0ebh7uHh6RkRn6S9EMczNT1aeqZs8+/Zb/J2ChPM/Zs2cplcu0m03CwQBrLZVyhfJ8jc21NTqdDuVyGSkl3XabylyN6vwcW5ub9Pp9LtWqbG9t4kqFW52n3u2lhl3BZ9Dt87CxQ8XE9ExM1S9Q73dwHAev6NMK+7T7XQrDBMBe32Vhbolmu4lIYpSVaFzub3ewQY3YKoR0gDRnsdjoAmB8l6jo4+kILUO07SOspNveRuo+r79yEUcpoiRJpWvyvNucnBeaAxuATibPYW2ak3KAdcwBJD3EhC1jbfaD/3wagJOhUWvthAGYTaSHleoYGoCz9sXEvqbOyR7n1YLAzDQA93PWSPZ+8VldBSEEDl46FjF9Do1Nhp+JY9kT+zu9v+4u6OPi8ZqJY456vkR6V6IlghjfTTi55EG4jUeC1AaBQhrD9RPzGCVZqLeY32pCkqRafLGmGARIIXF9j1JtkSgSfP75LX7CSRZXy5x8/W28epeBbSNdh8gkrO9s4ZcKeMPvb7vdHuWgDqIIKQTdbpfKwhxKCMqlEoHnI5UCa1EyLcLQxlCeq+EUAjZ2tilWK1T8ArVCOQ3XSolTdKjMzxFGEUmS4AqLcRWlSon19XWSdpNrt25Smaswt+AACQ83QurdFpGOEVajrEDrgIc7PQjmwC3gej2isIdUEg+J1+4TVQpECxXs1gaxDUmsj6c8BBFxp84rL53HdSSxPq5q+5ycnK8zBzYAxx6Z6b9nMUvg5HFrfX15Vsf7eImT3e/Yby+HF0x5GjzLUTwfR/z88oy9QjY1yQu+z4mlZZIwQnliqoXi56dPAPDyzQdorRHDYg7XdUf5cdYYCrUqH9++wbWrd4EfUZnzoFTASSyODlnfSEOo0WBAMlHslG0njiKsMQziGN/38X0fISRxEmNMehNSKBRIkmSUnzgYDEg6HZRSOI5DHMc0wgYAnU4HY9LCD99PhZuz0PHGxgZLS0uUy2USk/BgY41SMc2DbTZBSZkaadZiEGgJD7e22NjeRgLRUIAaC0mSENQ7RJUCg4UybG3gCEngeZQKJXrKoVdvMD9XI/A9emH8DC9wTk7O88rTKwLZWwLwAKt9fcISeQglJ+foZB0ulBUEymW+XMVGCdIwDAekfu/PT6UdQC7duEeSJGitSZIkLRDxfarVKpVKBa9c5KNrn7PxMM0BnFsocPnqF3xx5yZWCtbX16nVaiwtLQGM8gGziuAwDPF8n0KhMGxJJ9Baj4w9SAtEBoMBxtpUD7BcplqtcurUKXq9HsYY+oM+QghqtRqe5xEEAa1Wi0ajQRRFNBqNkYGZJAlz83OcPn2acjk1Mrs9lyBIc/ysFGgBGkuj16avYwrVEtJ3QQriOKbb7eJvp0Ujg4UKjlAUPZ9qoUStWMZXDnoQcnJphaX5ha+8AC8nJ+f54Ei9gA8ih2IFaHv40J2QIs0JPCayO/enweyQ8PEzkoYRkBzhvL5oTHYUMfn5ei4RgHI9vFhTLgRIbZBGoLRFxwk4irVCka1KCaU1Z67fIQxDpDEkcYzWmm63S7VaxQ8CuknELz56n7X1tGLYcSTGM1y9eYcebZaXlymVyziuS7VaHWn3xXGMlJK5uTkq1SqNeh1rLYnWGFJdwOXVVQb9/khKJkoSBr0eZ8+eHWn7Afi+D1YShiFJkhDHqbft3LlzFAoFPM/j8uXL9Ho9FhYWqFQqPNx8yPxiiSDoALC0fIk333iTW3/xi3QeBbTViCTk7voap08vYhoObuKhlMRxHIJ6uu5goQzG4FhFJSgQKBffQHNzmxPnLrC4uMSttfWhnEyUG4I5OS8wh7a0nqZkihDj/qrHsTxNvqp9wTe0de9TIP9xe94RmDj1+J0/cRpPKJRJq1eziPyvlxYAOHt/Ay9Oq2QzI0xrTbVaRSlFpVLhixtfcu3ebbCKbjsC4I13X+G3f/rXWVheQmvNw7U1mo1G6sUzBtd18X0/7c4RRWxubIz0B6VIPWylUolBr0c8zONbWF6m3+0SBAHGGAaDAYPBANd1kUqlnUE8j0RrwjCkWq0SBAFSSjqdzsjwevjwIV9++SX37t4D2wKg14NGs8urr7zC8vJSOkOnbYCxQvBXH76HWwzwCgW0McRxTL/fx99KvZ6DhQpSCGrFMgvVOVwpEdpCnFBwXJYWFtLiF0CpZ9leMCcn53njaK62A0lDiIO/d2o5osTJ1zaFa5/j3Y89zveoaOQQmzkQX9Pz/cwS3Z/lufkGff6llGAMUlgunD2DqxQSO6wDT/loOTUAX7p5D0i97LFJUu+cNvheQKFYBsflF+/9ima3jUDQ3BkAcO3mZ9zfeEiiDdaAkopCoYDjpJp8UkriOEYpRTz0Kvq+j1KKZGjcFYvFkVFngXgwoNVqERSLqaEoU49fHMcoKUc5gnO1Gqurq6N9lcsVlFIUCkU8z6der7OzU0+rgZ1UBqndEszPz/PDH/yA3/3tn+BINS7VF4Ivb1zHK5Xo9FOpGCklQRBQGFYCDxbKlMsVFheWwIAjFMIYSDSuUpw5dxbHcdBa54UgOTkvOIcoAvGHj3ZXpM54v7W4In3/btLE7f1qUg9nl1phMHLvMN9kuPpphmn38wTODkMLZl8CzawKYXdGdbVBoO0sW+Dw1dVKjJPx7bD6e3JrzwKBg9izutqCSJh5tEcK+87yiFjS67HHGjN2YwUkE5+9J09FEGDdGS/N/qw8v1iMjXA8iWs1r790Fin62IKl7SepF9D4XF6YB+Dc1RvEcULfxmyLNiWhqJgA2XfR5QWu9AX/8r0PCfyAs6fPYIft4OZLLspUaO5oFhfPsLK6RJz0gZhut0OhEOB5HkKkhle32yWKIiqVCkk0oFKpjLyEWmtq1SqtdpszZ84QeB5xHOMFAZ7rUiqV6LY6uJ6HMYatra00PBsELC8v4/slHNXHUQGdTki5VKLRaCCkQ62afs+aXcHD9TVOuC4/ffNdbn/xBb++foUwjsEkNFuK9c0mjreIEc3htQd36AEMFyosrqxSmF9ERxKjAWEJ+23ifouV5SV83x+1vcvJyXlxObClJYREiMw4e/wiEEibPtq9CDt7rVmt0Wb9t1/RyFcZEj7YPjNZkt3LbBkOscf5lEPvnwAsEsTu5fDHLvbc13h0z4bsPMq9F45yzmfva/a1ONwaEhBWjMZ3PBx+fM89AqzQ+K7i3OkTCJuANGiZamNeXZhn4Cgq/QGLD9ZT718cozFIqXDdAl6hxPLZC/zxr95no93i5OoJfu8nv4srSgBU5zy69Sa16jyDQUy71aHd7mK0YW5uDsdJjf5ur0dnWM1bqVTwPA/XcXEch42NDQaDATs7OwwGA3zPY25uDmMMSZIwGAzodbs0Wy0skAz7DWfhacdxaLc7NBpNhFAsLCxRrdRQykUpl1arjaNSD2A/9JhfWGDjwTovnbnA3/79P2CuVEZYg6OgF/W5fvMWc8urmOH56Ha7FIfdQMKFCouLSyAVVshxMMZoMJpioZD2Ec7Dvzk5Lzx5Z/CcnJyvDm05ubTE6vIqQtu0fZlNi3g+Xk6rdV++cx+bpB7OJEpwY4UjPVSxiLewQE9IfvaLX/LSpVf4m/+Dv4nnebSbaQ6gW7BIJVlYWGBxcRGAZrOJHubnpaHZMuVSiZ2dHfr9Po7j0Ol0QEC1WmVhYYF+v09QKo0MrjBM+/Faa/Fdl52dHRylKBbTCuKsIrjb7SKEQClJr9fl1q1bWGs5ceIESZJQq9Xo9wcUgjQv7+qX20SRoVKZ587te7z12pt87+13R17/fhxy88FdinNVomGBSRAElFupARkXfdz52szTXSwWMcbkBmBOTs7RDMAnLbr4qnrOflUFIzk5+WdtD4xFasO3Lr6MKx1cJMqCi8BqPSoA+dbdNeI4YjAYoMMYNQC0RBbLiFqN965coR6GfOv1N/nZn/5J6skzaWVuoeIwV5sDYGFxgWKxyPLyMpVKBWM0/f5gZKxloV5jDJ1Oh0F/QLOZhlYXFhZYmJB1McbQ6/VSDb4gIAgCHMeh3mgQxzFBEKSyMsOWb3EcE0cxc3NzuMNwsVIKrTWXLl6kXEoN3PqO4OoXN/iX/+rfcuWL66zfXeNv//7f5OKZc6luoePw6ZdX8BbnKRSLAMRxzGCrjtNLexm3KwFCSixDjURr0MaQRBGVcjkVsdb6QGoOOTk531yOvQr4oNvIRFiftQEoJwRgv4ox5Lx47P7c5Z+3lGHzRC6cOYOKYjwhUQYcJJ2gwM1qBYA31jaH1bZ9dJTgRZJSUMX6BWylwr/55S84ff5lVk6c4ttvvkmtVoWhAZiIHgz1AjvtDtVqFcdxCMNwKMzsj0K5hUJhZJilPXdLhGFIFMeEQ7mYOI5JkmTkKcz0CLXWNBoNXNfDcV2EECPDMMsxLBaLFAoFwjDk5s2baQ/jIGBz8xZKgtZw8tQbhKFGWA/fLRF1I1Yqc/zwN76LIyWx1tzf3qAT9ZlfXBiNQWs9CgO3ykH6GRvmWmdi2TqOKZfLI8Mz/xzm5LzY5LeAOTk5z5w0o1HgKZezJ09jomQc/rVweTUVfz7faLKg05y/OE5AW1ytKPplgtoc6/0etzc2+OFPfpdr129y5sxZtra22V5PhZHdAvSH+n1CCD788EOM1iMplkqlysmTJ6lU0grdNFybVgpbY1lYWCAKQ4rDVnGQFpZVKhXKc3PUajWCIGB+fj4VfnYcomF4WAgxMih7vR5bW1tIKTlx4gRnz55lfn6eSqXCqZNlAHp9j4drW3Q6fRzlsbp6mqg7oOR4/N3/8X/I299+Gy0sjW6H63dvkRg9Ckdba6l00srnRmF2eLdUKqUi10Px65ycnBeXp2gAChCKUbK6FRPL8e9LWIEwh1ueb+kMAVbOWGavI0l/QKUdKkcMl+nzv3s59MieE2acn2MfodhnycmwVmDNHsuen7m0iKfguiwvzJEkPYS0afGCdflgIc3Xe2djB0e5JIkhSQxGa6SN0CamurTC1Rv3+I3v/jZbD+uszC8yVyryrZcuEXVS48YtWh48eAjGMuj1OXPqFMJaPMdFCUkSRdTr9bS4w/eRMhVWdl2XTrtNv9ulXCxS397Bao2OE5SUaK0Jhl1DBoMBnueleXUCypUyxaFETCY2DQLpKoJSgc3tTfr9Hr7ngk64cC7N2UuSEu+89SanT56kWq3Rbne49+AeH/76AwpYfvd73yMQAoTh+oPblE+sYoOAMNGE/QHBTmr0bgcSKy1WWpAGgcGRAonF9zzcoYcyrwLOyXmxObAMjDygpMU4rJBVLabYKbEyzaSsxqxclIPKtsjRD//BscKgxfGN4fjZTw4nZi+rTTLdAWOyE4tGzrDzZkucPLL9bNsChDHPgf08w9NhDQc9pseTfZ5nfVVmS9G8aFiGxt4uBHJGg2pLyS9QKjpEcR0ROAivyEAEfDxs1/bt9QaDgSYcJEShQYcDFhY05eUyqjzHn//VZ1SXTpE0+pxYKXF+eZFep4/jV4AdCmXFSy+9TL/bo9nYxnEFxZJHFPeYm6uwtbGBkQ7z8/PIoWEH4DgOBc9HWoGNEmrlCnEUoaxl0O7iFgOifp9WKxVw9n0fay3RIKRU9Eb5gdbaNEyrE0ILX96+TrfV5uTSCjaKWVlaouDdB6DfMTy4fZVTJxZxnIBStUyZGlutdR5cvcKPX3qVP1pc5tr2Qz6+c5W/9t03aHsBkZWoSJPcvA8/fJV6ycG6FlyDiA0CTdFXuEJjkpgoikbHmZOT8+JyCBmYx+f4PT43cI8fhyfNJzzif8c5huPn8B6nTLZldHx2co0n92B99edkN8/SK5d7/w7Gfufp0eeEBVe5FItFJtWsb1dLNH0PP9GcX9tga3OLTqdDs9lK8+uCAm6pwnsff8zN27fY3t6gFLgoYanvbJMkCd3mgOy+zSvCw7WH3Lp1i36vh6MUJ0+cxPe8tBdvrTbSxcu6emQh46xaOA5DPNdFa02lkuYmWimx1iKkTFvAkYZY2+122rJumO9praVYKBCGESBRyiOONe+/9yH37jzA9YbFG+20oMPzfBKd8PDhGguLS1gruXXzNsIKfusHv4USis8+/wLpBThBgcRKhOPjbqUewGbBG55NS2aPi6+pWHhOTs7T44lDwIf3juU/njk5OYy6WBSLpannP16aA+DNrTr19Q2uX79Ot9ulVqvy5tvvcP6tdymsnOBf/+mfUJmr4rhQCCDst/H9tPjDdX3iXjrPFGsuURTxxhtvcP7CBYy1aQjWDzixeoIwiuj3+xQKhVFnkMxzp5RKw7uehzYGKSWDwYB6vY5JEnzfp1gqjUK91WqVoFBgdXV1lFMYRdFQ+F7x+WdX2d5qoKTHG2+8hRAuQSGVc8FWqNVq1Os7OI5DFMV0Oz2KxSpnzlzAdQq88fpbLMwtsba2SbMzwC9W0cIhMqA2UwOwUXhUMHws3v9VRTRycnKeN45kAE5WNGYVc9lkOYsnrRY+zHKc7B7DUfiqKkAzsWwlZG5y5zx1Di3MbSHwA4JCYSgyL9DG8NFimhN36dYd7t69y87ODmZYMVsoV5m7+Ar3dpo0+wN+76d/nZWVeU6fmOfs6ROjUOznn3/GzkYHgJv3vuD27dusra3RqNexJq2K3dnZJk5iisUipVJqhJZKJXq9HmEYEoYhQggKhQKe66bi0MN/K5Uq/V4vDQEbgzGGIAhSWRjfZ2FlZdRaLqsQ9twCb7z+Du+8/Zt0OyFKBviej+enGoDbWxFKKYw2aXs4z6PV6rJ2fwOTSKT0+e53f8Qbr7+NkB5/9eHH1JZWiQw0eyF2Iw1HN4LpdAWtddr7WEAYxaPjyg3BnJwXmyeWgfE8Lw2DCEG5XD7Aekff10GW4+ZJt/08aB7m5DwLDvu9FEKwuLiIsHY0LwyU4upiFYDTn12l2WwihMB1nFRYeRCysdPi3733IT1t+LNf/pxmcxNXJZQKLlEUUiqXOHv27Kgd3MJKidXVVZIkGXr3DPV6g2azmc5ZQ108Ywzb29ssLS0Rx/HIG5gkCY5S9AYD4mEFsSX1EFYqFTY2NoiH3T8azQYAa3fv0mw2aTabKKVYWVmh2x3Q6fQJgjKFoMLZMxdQwx7AUSTodGI8z+P0mTN89sVnJEnC9nadQrHCYJDQafe5d+cBb7/9GzhOwHu//piVU2dxC2VQHtG9bQC6vkMsp8+5NakPUBs9mq/zuSEn58Xm4AbgjAb0aXjDTCVQ7/f+5y4PZb9xPs/jPgr7Hdc39ZhfCIY/5F/5NTzcl0lIwclTJ9BGj9b/bGkeLSVLnR7cuEWz2URKiR8EqXyJVCSOxx/9/OeE2uAHAZVykatXPiWOB8RxTK/XY6deJ+yk++lEdU6cOMFbb71FpVJhZ6eO53nUajXCQUir1aJSqYw8eO12G8dxEELgOA5SSozW+K6LkhLP93Adh2gYOi6Xy5RKJaIowlEOy6urI3kYKSXdbpf19XUaO03u310btaP7xS/+kjDaAKDfVYRhSKPRYDDoUx5u7403vs383BLtTo/bt+9y+859Ou0ef+23f4dOp48WkrPnXyIoVoh2uogwDSc3C/7ENWGUC5jo1AB8GtGSnJycrxcHrgJ2Ju4WrRlOKoAVAuk4qRp9mOpQZUUW0u6d7WeExLB3Y/s0LPFkv1pCTK6vR9ubDnkM5VQOcxNsQYjZVZ8HCakc9K57/205sOfrBsTeFdpKyHEdsB3nBFkExqphlvgjo0j3s8eQhZQ8yXl4qohMEOcx45g4D09hCKgZH2W5R6XscDjsp04087zOlL6xIJ5ltaeZMY49EGBtwnzNwyZdXGlQCD5Zmgfg0p0H3N1pUI80JJKV6hK/8Zs/wF9Y4L/6039DvRfy7g9fp+AVePNbF1DmNI4ynDp3nqXl02xs/Vuk9YCYU+dX2dqSaBPiB26am4ci8Cv0B32ESI2hdrs9yvErlUp0+j28wEfiYaTASBCOYn1rC6+QijzroUdQa512/Wj3uX9nDSEEC/MrtFot6vUd4qjP6uIiL52/yNnTJ9h6eJ9b927wmz84B8BOwxJph1qlwpfXb/Pd73yb3iDk4dZDPrx8mYoXcHr1JMVikQf37zO/MMen9SYffvEFZ2plup0mpUCgNhskZ5ZpBWVWZI+YkEQmhCJAi4B6oz3KcfzKv6c5OTlfKQe+BVTIdLFipDGX/WtNGlJQUk1V2O7WoksXgbQCgUKI6SWV9XiyRaCQwplY9su/k4dahBDIGXmJB+WgeY37G4qZxM5ey97vl+mZQSHJzIXpKuG9tjVrDOK57qoihscrhdx3edpjzfQYdy9q4lpMLg5yZIwc/LxmV3Gv6/es+72aQyypFNTifBlhI1wpUMAnJ1L5l9PX79GONNYN0NIjwaGyeIJmP+Jf/MmfcPbSS1w4exFf+nz60WckGvpRwvsffMhHv/41mzt1Pv3oOgBWhcRJSLfbptls0uv1KZdrzM0tcfbcRRYWF+l0OrRaLcrlMoVCASEEiTFoIIwjEmvohyHtXpellWWMtTTbbZIkQSk1EptuNpqsr28xGMQ0mx1KpSquW6BQKOEqwcbD+1y9chnHFbz73Xdw/DRP8d69PlEi6PVDFhfnsSak1djA9R1OXjiPLfi0k4hEwN/4vZ/y+oVXaLQ7/NnPf86Zc2cJSgFCGJzNOgDNQoAQCoPGSgOui5EB/UEascn6GOfk5Ly4HFwGhmlz4KA/nXsLZzxOVmM/iY/DLIcZ2ZNu71lx+LHtJ8rxfB/r15dn++l6jq7hIQ5WScmpU6eQUuI6DjvVCg8rJaQxzH9ylXigkcJFOB4hgk6S8O/+6lesPdzEcwP++I//lL/8i/cYDCK2t+pI4XHrxj0+/eRzHjxYo92I0h25A65evUKj0WBubo4LFy4wPz/P1tYWN2/cIBpKtmQt03q9Hv1+HyUlcpgrl+X4AXS6XYpBgDOUf3FdF9d1aTabuK5HFEfUajXOnDkzajPneR6DMKTT6YzkZTY3N6lWUo/955+vcfXq1VHeoRCCkydP8tJLL1GpVKhUKpw8eZJer8e1a9dYWFjglUuvcvnKVfrGoioV2oMIZyQFM46wZDI1ylF5WkdOTs6Ip5YE8hw4gvZF8PyPMedFIw252xnLN4HsOISUKMdh5cQJtNa4rsunJ1cBOH13nc7dDcJuQhxZtHAwns96t8u//sXP0UYQ+GVKxSrgohNJrbbExmadBw92CAeG1157ncBN9foq8w7KEayvr/Pw4UM2Nze5fj31DiZxgrGGfr/P2bNn03xD36fb7aK1Jo7jYSePdMye5xEEQVpVawztToedej31GCYJFkvgB8RxjO/77OzsEIYh9Xod3/M4deoUSim63S5zczXm59MpuN6wrKysjApVisUiURSxvbVFo9EgjmNu3LhBo9EgiiKuf3md5cVVmq0eV+8/YPnSS8hiGX1vK91eoEaVvlnPYcdxiKPoufHU5+TkfLUcqwGY9dFUSh0pwfhpy75MytZIpZDy6GPNyXkaGGtGIbrJ5ZtiAAKj4grP86jNzREnCVIpPhkagK/cekC/3cexLp5XpDK3xMq5i3xw9Rqf3rjJysppXnnpdRbmV3ntW28RRYb33/uIJFKUC4u8/94nXLt2HR2lRo5bMJw/d3akyddsNlNx5zim2WxitKZYLNLpdEiSZNTaLU7iUY6f47oYnfbe7Q69eABYSzgYIKWkXC4jEChHYYxhbW0NrTUnT6a5e57ncefOHfr9Pv1+n253C9e1qUagqDI3N8f29jb1Rp0PP/yQq1ev8hd/8Rd0Oh2MMURRRJIkSClZWV7h3W//BqGGf/vLX7J86RKRdLAPGwC0St5IzNpaS1Ao4Hk+vW53NAd+kz5TOTk5h+fARSA5OTk5x0mlUsZTCtdxCGPNJ4tpAcjpy9dZ74aIRFAsVCgvLuNVqvzTf/JPiJXH26+8we3b95gvnaJWrNFv73D5sy9IEsFPf/qHXLv2Oasn57j06ingM4QylGsFTLKIEILV1VXiOKbRaFCtVml3OiOvXL/fZ3V1lSiOkVbiuqmIdDgUek6SBNd16fV6FItF2u32SOfPcRysNegEer0e5XKZOI7Z2toiCAp88MEHDAYRDx484OLFi/h+G5B0OoJ+L2JlZYX79+/jBz6u49IfDNi6c4+5M+fSIhPfw4QxUkmSJCEKE3A8PvnyOrWTpwjm5ulupCHgVtlP5WscF2do8AlH5WGPnJycEUfSAZzMHhuxS+VBzKiwfRrTz2PDGTOUKATjfwXjQpX95TMEjza2H27hK7+hnjE2O3tsT3Q9vnLZkf3Y71xMXLOvOGfOTv33dedw5/zsqTMszM1h4pi7y0v0XZdir8/CvYcYDXFsiGNDsVLj5r37bLc7/Ognv0u5ssjd2w+Yq1YJfI8z585w4swZzl68yFajjlcqcOPWHVqNAVanU1yzu0W9nhZIeJ5HsVik3++nmn7GDjuSFCkUCmnrtuHrYRhitCYcDMBafM8jHAwI/IAkTkjiGKM1Ugj6vR4Wg+MKQBMnEXPzc7RaLdrtFqsnV/jeD7/L2995CyEFc7X0rLXakpNnT9MNB5w5f4Fz5y/SD/ucOLXCb/3WjykXq5w6eZb799ZpNrrcunUfzy/yg+9/n3fe+A71Rot7mxuce+kSpW4qA9MIFK6jMCb1rirlgHSwE3NlHgbOyXmxObABKIU3WpQcP3aFgy8kvpB4CBwDjkklMNShKxqPzmzRWXfPRViFGo7VMeAicUW6zD4pgtRpusdin14HkoOfL8Gs45281GmV7PA/cbRxK7v3IuFI4fnjJTsPM67V6Jrtfs9X84NoJv77ehuBB6jSFy4CByU9ziyv4vR6VIHPlxYBeO3BJg+2mtS7A6zrUZibZ+HESX7+q/cIKlXe/f5vs76esFBZ5dsvX2R7/Rq9eIvi6TKr3zlDvGL46MEV7uzU+dmffkI/bY5BoVzAGMP9+/dZWFigWq2yuLhIoVCgVErz7RqNBgDnL16kNjdH0fcRUYInFC4SPYiwUULJC+h3OhDFVAsl5stVkn6Ii0CR0Oo8xNAjitpAwjvvvM3qiVXq0RZtUee7v/MuK2cXufhS6vFsdx02RI8vmzv0/SI7keY7v/09ikuSZr3BrcsbXP34AdLMcf78O7z06ve4eafOres3OXtqhcGgxZUrn/Lqm69yolAAoBUopLRgDVK5SL9AlIARalSEkqe+5OS82By8CljIUbsmEMO/RfbXxH+Mn9urI4AQx/47O9v4y0azh6QLIvX4kRlEInt2vz3NWI6fw3cQOXh96RN3N9l3EaNr/9Ux+7o/KnPzOMmbnIPz+PJfgUg7UmjDydVVTK+HtPDxair/8srtNVrtDtJ1wFG88tpr9KKQX1/+mGK1gpWCly5d4vTJk+gw5PTqKnPVCj/5nd/lr//+77N0YpVOv8cgSlCOR7eVmtTFqsPi4iLLy8sopWi1WgghCAIfbQzz8/PMzc0hhOCzTz/ly6tX05ZxdliYYwxYi4DUG6jT6t5Bv5/ebjgOgR/gKEkQeBSLAcVSgdde+xa9Xo92u0OlXGbQ69PpdHA9HynTcO1m3eAWCmgkJ8+e563v/AYfffIJ9x7c5/r16yipSOKEQlCg1xuwvb2DkAqQvPraa7iezy//6i+ZX1zEDw1SG6wQtIaVwGnxSoCUecZPTk7OmHxGyMnJeWYYY0CA8hTnL15k0B8QeQ43a2n7t3Nf3uZ2s0liPBYWF/AKAf/0X/8rmr0OS3FMo91kYa6AFZbWzja9ZpuVi6dobnX50z/+Be99/DEnVs8y51WQfUuvlYpTb3ceksSVkcSKUoogCNja2UCrJA31DgstarUacRwTFItgLXEcp+Me4nkenU4Hrcdt1aIoSitsSSuH79+/T7FYZf3hDnFkePhwHU3IqTOnWbuzxmCgqaaHzJ21PidOvExjq8/7v/o1BVexsnKKYhBRKjVAeGysb+C4DrW5MnfurtPr9TDyHV5662WkE3Dz9n02d1r0BwnlTkSrFtAoeiyT3vB5vr9vr/acnJwXjyMZgOPQgcVYw2Ti16ywwuQEKhBI9XiPy1HlLw7qMZu1ZSnlyCF00DEIIYbdMY4+7v2QT3HbOTnPCotFCYXjKFaWl0mShC9WF7BCcLbRwtx7QH8woDg/x+qZU9Q7Lf70V3+OFoLPvvyCwvwCryyeh+aAl5cWWK4tQii5dfM2YqvJ7//0b3Hx9Al+9s//GNGPcGgCIaun5/D985w8eZJGo4HWeqTJ53keURRRKpUIgoAwDAHodTp4nofv+wwGg1F/4E6ng5SSarVKFEVEQ2kVx3WJB6khmcQJCwsBjXqHpaUT3L//gMXKHIuFee7eXGNgNJUfpnPix9e22OxtcGLpHNXA8OXVz3j7jXOoJY+5uRorJ07Rau1gpeXkqWVKZZ9Go8lWo8VSb4BwCzQ623z8+VW0dKn0Elo1qBdcFrQmitNKciElQopRJxCl1NS8nJOT82JxpCKQqWW/12aEGtP1Zr/3icKTj9nmwbZ9+DDm7mM6To7jvOTkPA84yqFcLlOpVLl44QLWGi4vLwDwrTv3uHfvPkpKLr70EmcvXuDP/vIXtLpdIq3RwKuvvcpbb7zMb//oN1FaowcJZ1bO8eqFNxm0DYO+JRxotrZ3KJYCdjbTMKsTpAbf2toag8EAIQSDwQDX9RAICoUCjUaD9fV12u024VAcOpOKySqEM2MvSRLW19fpdNJOHoVCAdd1KRQK9PsDGo0Gvu8PRaQjet0ecSeisVZHaoXnJEgJUSzY7jm8+cbbnDl9geWlU1w69wqnTp5DJ+B5LtZGaBvSbtd57/2/oNttIh3L+nYdVMBvfO9HaOny3oefUFtYodpP2//Viz7W2qGnMjX0CkFh1Ls9v5HMyXmx+WpDwLPmn+fFxrG7/s3Yb3x2xuPn5Zhmsde1EPu8Nvn647b3dToPOcfKZG2zALTRRHHE2bNnmZufo79huDzs//va7ft82WxSrlQ4/9IlWr0Of/HBrzBAMuzpffXLK/zg3EVUPxVwRhtu37iLmpuj4FX4r//xP6HX2MHRPVZ++rucrawADYKKoj7U61OOYmNjA4A5rwpO6mH3vFQ7D0hlVqJoZNQ5jjMShg7DEM/zkFKys7NDoVAYdQRxhCFJYgqFAuvr6xgtqdcbdNpt5gnwqi4DKxjEW0CJBxsRq6fPsbJ6ii8+ucFcYYHLl79g4+ENvv/9V7l/7z7nLs0zt1Dl4cYDSqUCp8+coNXuYkKBQfGf/oP/LUnS45fv/ZITQYGL3R8C0Cj5w7M+vgbVYdw5M24P3Q89J+e4edx9SP75fGoc2ACc9DxN3TkKmUqnMLyOM+4qhVCjJGo7fvcj6MkfjH28XUe5e51cxyJmVu4KYUbHJHaFivW+1pDMNp4Wk4zCyNNh8idl5rXYFzmU4di1LSxqhvWXbvvRc2QBK2aHjrK9pOdwfB7ShHqeqy/0Lh/26NHBRVkks6/twcJrk2MQ3wg5mGmsSBBWYK2DUhKlLG++cxE3CLlVETQCDy/RXHrY5nKsWD13nlOvvMZ//d/9v7nTbNI1lsRa3F7IonHo3XuAtorOTod2a8CtLz5g4dRp2laTNB6StBooadhcu4v0Es58r4rwEza2N9DrCY7r0ut2WVxaIkpiAt8bdfzI9AB93x+1cAOo1+tEUUSxWEQIyfrmFtoYXNejXJ3DdT3CcMBOs0mj1WFhfolCscziwgmUW6DfTxhsttlotKFQ5KXXTgBtbt6PaHUNH177knanxfz8IgsLZS6dXsaLBE7Yw+vXWfUsGzLiXNWj//AWRS8Av0jU3uKf/X8/58tbt9hsNmj1mnitNrDIZi1AW4FFYHSIkD2Wa2XKrk+UxCTGYoXI789yvhKyiJYUCjnxW5wJ3xtr9v2dyXlyDmwA7p2Dll48hBq9ZuzeF0xOFKRaDHLPC5tuO3tlVsjzqHlwj67z6OELQJoERuMbVi6no0Pvud9sjGo47qHBS2oPWxtxXAbg7nOSeSwez6xov8WZ8SWzNpUo2YtkhuifgJHxnH4+xMQ6GvMcWTdZ2D5laOwOB6/Nk5/XgxiA49r5zOh8jk7QsWAxMkRYB6kdrLEoR/P2dy7QCL7go3fTdm0v1+ts3G7SC10WT13gYS/iX/zyVzRxSJTE17CkfH7npTe5UCzTaNbxai4nTtVYPVfm8pXPqW9tU1Yu82WXt99+B2s19c02UMUvSS6+colqtUq9XufG9Rt8fu0L3njjNdxC2hUjy+/zPI8kSVBKjTqBGGMoFos4jkOSaBZXTtIfhLSaTbabXZQaUC6XcIMiVrpEGnB8vGKZVqvPw60GtUKFnc6ATnvAd34QALCx7fL5F3dwVgTtrU1uX/+MRQQnfc0gWeQnr7/O4mKVTV9zyn2NV159lXt37/HKyxf47u//LT578JDB7ZAwiXALLram6b2dVv/ePVPmV3/vAhf/6QN03AXaXDpziqVylbXNDVwhiSc+dbnxl/MskVLiOA6OcHHtsHe1ZdSBJyGZ+TuTczwcQwh4L+mV/bAz3/FsL/PsURxtfM/HUe3N7GsimO2VS5/ewwDf55ie57Nw/Bw0Bv5ik3n9jUhvwqqlEu/+dJlmdYtr6l0ATi9sccNdwy+XOHnhHP/25z/n5p1bGGFxHAdpNaVCiVajwZ2ddbq9DqValfXNNZZPnuDC2dN8+5236QwiPN/HdRyUUiyungd2cIuCwC9SKlb5kz/5s1S02UqkcIiimDAckCQJxWKRIEh7+WY3V57nYa0lDMNUP08p6o0Gi4tLKKUoFAqj4pGoD2ARUtJqthD2Ie+992sWF1Z4/70PqO/U+Tv/6VtcuJQAsHLS4/RCiT/8e3+Ta5cvM68U9Rs3eWl5Gd1q06xvUi4Iir5DrVwg6rU5fWIJYRJuXv6Yy3cf8N4Xn/Mf/0d/j3/yj/5vvPN/+immmN54hDh0l3xu/q2TrH44wA4GnDh5gm9/+9s8+NkfD7/b+ec05/lADOMf2W+OyD+hT53nTwn0m1jk8A08pKNx/BqQOc8/cniDYZVAeQ4/+clvomtNYiG5I+YAuGS2Of0/e51CrYIs+vyrP/rXI+9xFEVoozmxeoJisYCO+vRaDcJui0G3xaDTRKGpFXwWSgXmiwFxp01zc51Fcycdg4Rvzb1PfeMO3c4AcFheOsn9+w9xHDXqDpJ6+JKRAWitHbZUcxBC0Ov16HV7eG4qBVMsFnn3Bz/gwsWLo5zBS5deolQs8sWVK3Q6HXzfJygUcHyP/+Q/+xY//v05PJkaad/97YD/0R/U+MGlC7y+tMjJgsdKyUNEHVr1B/jK0G83sPGAou9goj4vnT/DYq3EymINZTXff/cd3vuLnyNURPmVJXypkdagsBgp6Jwu0Ol26DdbuAj+4A/+Fq7jzIzW5OQ8C7JInh2mBu3+O+fp84QyMI+GYydfO4rEgMzEovfY9pNy3DmFB2W/8/UiISfyAF7k8/AikaYEWIySWNelWCjxH/zdfx/YQWH5j82vucsci6KPXSxw9uWL/NWnv+bLWzfRVmJVupHAK/C93/weSgpWF+YoBw6nz52hp2OCcolWt0O3vkWttkg06LNcKrKw2uNHr7S4khTQjqJa6fMb9kM+WV6hUKjgeQ4ffvALvv36KTxHE4V9CoGHkhCaLtYklFWE1TFWa1Aapwyep+j2BsxVKxQLPey1f46vI96tRdhkgI77UDD81mqA1Z/znW8n+G6XP3hpnqUTGq6vEa2kfeC0lHz/x4p//Y/+G6rFABsNMO1temiSfoNSrUrJdzh//jyffvopd+8+5J03X6NQ9PEVvHzhDBd/60f88r0/RyRDjyWa3+bm+F7LWmw/ot/qsNm8w+lTpyiXK4SNBok9aKpDTs7xkuluGgxCipHRF+t44l25D/BpcmgDcLcRNfkjfrTihKmtT6bcHbuBMMsAfJqGyGiXE0bti8vQuS9e9PPwAjHstpPVSC0sLvD2y9/lnvk3SGG4QJ0L1MGA13Q4ffE8/4//+3+Tdh0a3j9KIdFGs7C4wInqHMHWbUoLNSpFn4unznPlxpf4jqBYq7K8uMD777/Py6+8zOvnBlgLTqLRjsJECZVBl3/w/Vaakwz8J68VgI8Of1wLANvjv7NugjOJ0n866c+Zam+g/+ou/P0f4xckhV6b5UqBtfoWp+ZKKDS+7VEsuMT9DusP7uIIw8nlBeYrRbSF9s42tVKJjz/4FRtr9wibbaL31/DePcHoAI1l+XIT24+wcUKUDPjVr36FNXYkDZOT85VhwZBKFeV23rPnK5CBGVWC7P3S7uf3eJ/Y67XjDi3OGJ9gZqHz0xnHYxBCfP2MKXuA87ibPHT83GMnskNF+sTkXyDg7bffQnUSaneKNL/bTd8kBE4Ipz6bY7vicfPBPTR2WLSefr4X5xf48W/9iPq1aygJCsP62n12GtuU5qoE5SIOLp3GDoN2g/bWJuqiRgg487PLyHKAM3TEyz0+S8YKtBXDf0EbsEgMMv3XCgyCxIBBoo0gNpAYUI6HNqCtIEk0rVaTMDIUilXCyLC51aTbi/CrFX7vD2soT7D0f/0T3CubOL//Jg8Th4owqLDParXI2bMnEMKwte1jDDjSTfMLA5elpZN88dknrJ48RWnlNAvzFe6sPaS+uUG1GHDm5y2cuTnWz/tg4eRnLc792w3mzp3FkYrl1VXe//ADev1eWpWff69yvmKstSQko7/3b8eac5wcugoYdnX1mKhK3S8cPElaGbt31YFCjwwDNVmBay1mooRUkXbrGCWXH7Jzx27G3sFUrmKvCk5hLY5IHnkeUomT2RIx0/uZdb6OwnFv72kipn5vpiuEZ+Uj5TVgXx+0MmjSHtuBcjFZq7RiEU/AyyeX+M//3h+i712jtqUpb0REJw1SK1b7y+jyHJcfPOBht0mDHkZ5WByktSj6RJ2bRN1rYLpIz8X3FG5JUJ3zqcxXcRJFEcG7b7xOp9Ph+pcdFueK+Btt2OliXztB4ij+8R8Jvry/hRuU+PZb3ybWCUFQwFrLzs4OkBZ+KDcAqdjZ2cEZ6gFaYzAWgkJaLHLnzh2KxSKlUinV1cPSqheIBhEnl1fptbp8+vEDqqUqF0tn+Xe/iPje33KoOg4BID/b4PLncGZlBWu7FEsKQ49yqUQ1KdLrdOm16wCcPrmAkKCEIBo0WJk7y9yZBQqrS7xx5gzFsMfry2dw/2yH7/zcICJNtzWgi6C0ukQyV8aWAq7cvcUAQyLHN2H5T27OV4UVZljtmzL5WfzmiWI9XxxaB3C3gTErJLx/vt2EZt7ktrBgBUJM+RHSdSb+/+jzQ3PtiKHjR49BkEm6TI/PoOyjhQypLt7BElf3C6EfhScPuz8bJlSAxs885nrlxt/XDKHT7y8AEkyEkgIMvH7pLP/n//1/zqsLBQa3bxDQw/YN/m2D6wSUTs8zKFS49eA+rX6fRJhUiskKrDXEgx7Xr37EnO6jHAehBMpTBAUfz3exJPR6XfqdDjruk4Rdrl8TnFkIOQ1gLYnj8N9/5GKUw8WL51GBh+NLPFEmjmPa7TZCCMrlctrbV2uUdFk9eTr9O45JjEklYpx41CYuayUXRREP7t9HCcmFM2eplMq8cv4ihCGteoP7N77k9hcDnParNOuWd4Gdf3GTcOEUzMdg0lyoJAkxxifwPQbdLo4jEUAU9nEch0q5RHVhDr8gWb9/i3/5s7/k/rWr/K//3t9BWYvrumg9AAFaQGw03bBPbf4sOwZ6UZjeNENel5Xz1bJHOC+f858dz18VcE5OztcTa9KgqdWIJEJZw2K5xB/+1nf5v/yX/wVvr86xfe1jCkkbJ27imAGO4xP4JVSpBm7AtWs3iMIIhUylOC04QhC4HoHrYYeFDo6j0uraIKBQKKS7txrXFUhpQGgKBZ/tu+kvzE7k8Ud/ucCNG110t0tra5Ow1UImmigKsUPDKQgCXNfF94ORcdfv9wnDEKUU5XKZ+fn5USeQMAwRQhAEAdZaNjfWae5soTC06lsQh0T9LnG/R7/dwJOwVF2i6a0AcCIeUC4G9Hq9kcxM1nHED4Kp05vJ0kgpKRaKFMoBxcDl1OICr58+w2999zeRUiN29VnX2vBgbQ3UhMsvJyfnhefABuBItVtKlFJIKZFSHrj37uQyez257+3ontsS4hHlmCftLfx17bd73D2Dxa7/cnL2wxEGhcaRBikSVhZq/E//3t/l//gP/3ecCnzWr11BRX1IemAGaJ1gEkGSCJrbLUItuXXzHlI4OEiUBWnAsYLVxUUunTmPJxRKjOcPbTRCChzHIU3CiPEDRbkSgEjwTNoLuB35eAYqjqTmu3Q212lvPMSmwn0jIeg4jtnZ2aHZbNJoNBgMBvR6PaSUaJ32Ew6jkP6gj+/7FAoFtNYolUrJnDl1ku//xjusLNRYmquQDLo4aL792ssEClYWqmyu3eXzZhOAWreFSELCQUgURQghRvOr67oUSyX8oQ6h1pokSdBaMwgHdAdtHKF5/dxZ/v2f/ITFUgHXFRgSEIzm6tG4O11cxxm1g/u6znM5OTnHw5FyACfJ2rbAwUOQWTPy3aQ5bHuvM7MrCMBEq7X9jJ/Jse7H1ymvbpKnkl84KduSO+dz9qHkO6AN1WKJc6dO8w/+/v+S1196mXB7A60jqsUFVNwm0X1wDdooosgSRwOi9kOKxQUePtwCFOgENcwZ9ZTiW5deQSQGE8UIJy1w0Ebjez5SSvphSK/Xxgx6aJMgpMaSUFYhAI2eQag+K5UyvThiuVzESsGda9eYu/AtBmFItVpFCDHS/Au8Ao7nIYSg3W4zGAxGRt/iyiKdTmc0p2RagS9fushyJWB7Y4P5SpVf/vxPmS9XqJUCTq0sslArU3AFKy+/TPzBPVytmR908U+fwBhDoVDA8zyCIPVAztVqmChtUwfj/Os4iQjjHjK0LBfL/Pjtt/FNgjEhiYlwGN+sZ8ZpEg4or5Q5d+4cN2/eJEmSr83clpOTc/x8BVXAOTk530Sktfzo+9/lb/zeX+f77/4GvnLRUUigajy4f5OV+RK14jxGlIiTHmhwEpcotCAdvvzoMxo7baRwsSZBMswdNZY3X3sdtKEYFBgVDFpIdIIxZug1ExihQWgcV7C8PM+82wCg07cIZ4CxGk8JlqsVuoMBX9y6RVya5+Sp02ml7bAfsFIKx0mnxzAMMcawsLCQGpv9Pr1eb/S+LAS8sLDAztod5qtlbnz+Gfe//JJaMeClC+eI+l0qxYBuq04kQJiIDeVyWmteLQU89DySYeg326bruiSuOzWW7N84jgk7Dc4u1Sh0FcUgwNiQTjxAE+OQ9jAWQuB5LrHjIITE933OnDkziuAcRas1Jyfnm8HBi0D2qtqFcab+MUUT0nYwk9s/gMcu2/1j3ip2VRVkSdDHgp09hpnN5Z6Lm+9hQc6e53lvmYjsfD8Xwz9WxMRBSaaP8Ot6tPvlfR1dBmQvr/9vvvk6/4f/7L/AVS5WSe7eu89nH36CbsNHH/6afrvJf/C3/wZ/7Yfv4voVSg6Ibog1EUp5vPfnf8FOqw6ej5UOFokDlAKH11+5SNR8gDY67T9OKixe8Hw8qYj7ESZOwBqENWA0ShiKMs2bM14Fx5EM+gOMBQfLysIciVQkyoUoJuz18ApFjLVEsaFbbwASCyRxzIN793Fdl0KxAFLi+R5SCrqdFkuLc9SqJXpbkl+//1dc+ewTzp86TTFwiHptlBAkYRffkQSuolKqYFqn4PYNio064vwlfN8HGBWUJEmCFRblKZSrsBiEsFidpMLUsaHbaqMijS8ckiRGuaAcfyyNYAUmMThW4To+AsnC3DxyKB+1+5ORB4Vzcl4cDh4CNu6ez1sLekLDZ79Q8UEQwh1NQukE9fj1pE3gAIr2k7IyBoiPUQhV7lOyqqXa+yVhh5pk6avHfTd+8K4ssz4GergMt5eljAqmnj8OpJgY61cmUDt5HsZpBbvPw9NESjnT1DzS58Puc23Fkx1Tep3GJsN/+b/437C6GUPR5b/6V/+Uf/bHf4zTM/RigzEOUvv8o//nn3PlbosTqwF/46+9w6ovsD1NGPf48JP3SIgJjQW/iBISn5jTJyqcWPJpDwwRCY7wwFoKjkfJ8SnioBNQscUmpB5CDQURIQTEVmKKRbQ1ONLDsUAE8aDPUrHIw60dksTiKJe1jQcQFFBBgYLr4klBojUi1hSUi0QS90MiJej0u9RKAYqYu9cv4+kOJcfyyft/wTvf/jZLcwsUHRdfSUwUQ9zBcX1OnzpPFIV0qmk3kEq3g2Vc5AHp3BfHMVoanKKDFzuYTogOYxIMgacItyS4lkjG9KNuWixjAnRsidEIq5DG4hkPIoHoGERoWJlfSOdCO9RazGQac2dgTs4LxcE9gHvcG+6+f3zSpOK9JVL236YgzRs82J6nFYaOi8wbttcYZnr/YOzrfApizgeXh5k1vtnX9rhzAZ8PKZu9zsN+V/bpjWGvr9GRO+vsydM5pijWJGg+vPwB/+aPf0a91aEmAqSjQLtI4dFp9/k3f/Tfc/pkgYJt8ePXLuGqItfvP+CzWzeQ0sV1PcLIgIQw7vHOG7/F8sICzVtX8BwHkYzHrqRM9SSNGQqMjouXSjJtKdXVHspxwOhRsYfjOmm1rZacWC7wYHOL0uIylWJAK06IwwF60KfgpV65cJDmEmqtUa6DLPgoxyHsD/Ck5eULF5EY/uU/+//x5muvcfrESVwE1aCErxx0HPPKyy9RLBUIwxDf9+kvLQNQbDbT8Q9v2LKCk3SsIpWYEiCkQA0reZMkod/tEw4iHD/9nFprSSKDlhbcNICeFt0p4kQDAuW4nFg9gRAiFYLOyF1/OTkvHMeTA3iAyeOpdqz4ZsYjc3K+VqxtbFEpF/jo089ptPsor0gSQxzFCCtRJiG0sFANeO311+m0OtS3WxRrLpdv3aKjLZEVCOuA1rhSEDguL5+/QNTtIY1FmL1vTIx9tLVZUaaFEz3jpbl1jqJYLI6UCIwx9AcJJb+MsZqba/eprJ5EDCISnRAlhvrW9kj+RWtNGIZ4vs+g20VgOLm8iIuhsb3DzuY6J1dWuHDmHAXXw0HgKwffcbFWUDtxEm0TCoUCrVYLPTePVg5KJxS7HXqVtDo3MwABRObFt+mxSqWwJg1J95IoNSYdB2fYMSWOdeo1dPzsDKXV0slQpl5JTqyuotSjOqc5OTkvFgeWgcmShicXJRVSPfp8tkySTbq7ZWQOus6s0PLu9x3UCylgauz7vvcAMjUHHet+zDonxyHXcNzby8mZDNsD/NX7H1GeW+Te2iahhlDDIDLEcUKSxGidiih3O10e3H9AwS8i8WgNYn55+TKNRKOli0kknuOjLJS9AudPnaVdbzBod3GQaccgpXDdNC1Fa02v20UbMwqTK6UoqdQA7OMjpRhVw2YVtoVCgblaBV9Z5itFip5k/d5tHBLa9S26nRbGGNrtNvV6nW63S6fTodVsosOIXrNNc3sHZWDrwUN6zTYnF5exUYQJI0RiSPoD9CDERRCFYdphRCmCIEAqRbc2B0CpUR+dx0wKxlgz6pFqhx4+ozWOo4iiiG6nQ7fbQSgHYzSe52Os4cGDBxiT6SU6KCVJdIIUgqjV5uTp09SqtdH5y8nJeTE5tA7glM6cmNCKe4wG3e7XDqLTd1BNu937P+ABjdY76HEfx/sOsv6TaBgeZNs5OU/KXp+nK9dv0h5EFKpzaOmQWIWVLo7rAoI4idEmIYoj6vU60kqEVfQ11AcxkXRTA1ALrDZIbamVyizX5rFRgg1jhJ6WfMrkjtKgdppUkY2pKIYGoPVRysFxHKSUow4eruumnUqSCBP1KQcevVadfrtJMujQbbdG26/VahSLRQaDAa1WCxdJ1OtT9gLCbp/G5ja1YhlloN/qMuj0iHp9TJRgoyRtLsxYbzCLhnSGBmC52Ridx0xaxgzXGd+4ydFxDgYDtra36XS61Hd2KJTKowrlu3fvjrQJlZIUiyUqlQpKSdqtFrVqlVOnTmL0s8lrzcnJeT45vk4gdtcy6/nnLVT7uLE+r+N+HsjPU84En934kgcbm7zx7XcwKLSQaCTaGCyp4TOZSxxFMXFkCEpl/lf/8B/yg9/5HRyvgEDhSgclBEvz8yzNL2BjjU2G1a8HQKHxZVqc1jf+lLRLJrOSJAlxFJGEfZKwh6vg7KlV7t66jjCaJImx1mCt4eH6Q65cuUJQKPDKyy8z6PVxhKRUKLCzuUmlmOb6Rf0+Oo4xUYyNYmycYGONSfSe1diduXkAyq1G+sSE4HOSJAhSL15qvA4LN4B+v0+71WJnZ5vA8xAm9Rb2+32u37jN1tZWKo8jFdYYojDk3s2bfPD+e0RhyMuvvDI0zCF1MeZf3pycF41D9wKeeg6BRe1dIGLBoMf9Xs1YSFgg0GK64m2v/ezXd3j6NQE2s2XtrrGOJ7ap/dhp61dO9qY1s8ocBFqMT9n0GMxQZ2Z6rALQxzCxPh+FEhNYOXW806+ZJ0oqnzx3e0lV5DyfKMey2XxASWkulh2utbboKEFsBrh+QMEmOGFCzfcQkSaJQjphnZpc5URxlf/5f/j3+da59/l//Xf/LTvtNQJHc3p5ASeKGLTaxFGCRKImxM6FEMRxPPo7ozD0/kVWoYWD46iRIaWUSo2/OE7bvA16xGGIimOKJuJbqwvcvncXUZ5nEAcozyWONcVyGYRDvdVjYA2VaoW1h2v0mjtcXLpA2NiiWPQo+AV8V2BJSGxEjEDaVOEg8wBmaSK9+QUAyq1mmt8oBBKBSTQmScC6KOWhlI8gxJiQONIksaXb7pO0Y/xY4QxAIwk1fHJ3g8Xrtzh9+hKOUIT1NmG/x9r6Ok3HYc33ePXkGWwEWAUkme/0mX1WcnJyvnqeKAQshECikDiPLAoHhTv6W6AQdrigkOLRfLTdeXSzXps28DIdA0lqximkcCaW2fmB0maLQA2PRA3FTsavTS8CByEcUtvZAdRwGZ/KqbHKY2jJdoQcx6ePnLE8ebhaCokUacjr+TnenMfRj7rUOxu8cfEUf/sHv8GKk2BtA+3EJPTQokVQSCgWHQa9Abfv3OfOxjq9VkzFzqMaDr//w7/BH/7+/xDhWowNOTFfI263GLTbGG0wUqbpGxOpJEmSevqmDEA5Dv/unlcy4yvztMVJjE0SZBITmIQTBZ+TBZdBa4dWcxvPS0PHVgiE49Ho9FhvNIiEZW39AadPrtDZXkdFXQJhcKVFCgtCo0VCIhNimYy+GpN50HGthlYKpTWldhtlU7kqoQ0kBqxACBcpHIRQYBVaA1YSDTSNzQayZ3AjibIKjWKj0+fW9g7WDzBCYuKY/s4OvfWHlHstwnt3uH/lKsKIVCrGMuq6kpOT8+JwLFXA+0nEjF6zmcfwafhzDiZlcpA1stUeL+mSbftFLkHe+yzlvJgk1nD1xk3eXL3ASmmBn771Q66s36UfaSrlCtZatjc2SeKYgVSstwdUtjTlFUs93mT1/Ku0dZ+CU6FSqmL6MSsry/S6XeJhn1xjzIFuW4silW3pG/+R18wwXKq1RpvpkHKWI7iyvMydcJP765sorYitpNOPsMJFOR7VajUNHycJ5XKZxs4Gc3OVVJIG0DqZKrLY/a3IjEDw6NXmqexsUW41GAwrgWehlEIhiKKIwWBAFKaVwHEcYZSb9hCW0Gi0SbTGGIPnuqhRb+EyW9vbfPjhr9NCkdzqy8l5YXliA/Cg8i7Z+/YyFp/mfp/Nth9fIJIHMnO+6cQIPr7xJSfLiyTNHmVZ4Lvn3+TimXNcfPklWoM+/+0///9w7c4tujrm9k6dheWXsf4CkQ2oN3s4RcW7336bn324wvZai1KxRK/XgyRJ9TKnBLpTYy5rjzYdAh4agEMPYFYcksm/JEmCHhpIk1/NJEkQQuD7AS+fOkvjs+ts3r5HYX4RHJf7D9coV+dwHUG30eX84gKtVotSqTRu2barD7e19pHnMgPQGENvfoHKzhalVoOt0+dG61hrMcYiJlItlFIIbUdt63r9HuFgQBTFWJWlTAh2mt2hZ1QSBAGnz5xBb6xRqFZ5sL7Dw831oeBkPi/l5LyoHLwTyD5dJQ6SnzZZMWiEnZp3Zm17d5XhrNfsrsl1Fvttb9b79tv27lD0rBFIOZbbP+hYnyYHPQ85OYch1JJPbt/h3t01zs+fZGVxCddxcFFsbW2x3e/Q7HTAd+l2+qxvt5nfqbP9s5/x0qV3WFiq4wYJD7dvMRh0iKKIn//5z1n54Q+pKYM1BunI0Xco7XPrpQLISZLm9g0delkIuGf9VDxZpL11pZRDj1k8MgCNMSPpFaXUUITZUFEFLi2d5NbGJt1unzOvned+s0lkNfdu3+X0whxBEOA6Dp70UUqgjcaVHq7rorUeeS0ziZqRELXjkCQJYRjSrlRZBUrNxtjLSfq9jOMIJdzRTamUkn6vS6vVRilFGIY8XF8nGYRo34GiR1DwKBcLFItFvL6mZ3uUy2X8pk+lXIbNOnY4ltwDmJPz4nLoIpCDF2bMWH+/DhMH3PbuvLCDGlSHHet+79tdaDJ7c9PdHb5q4w+ew4KSnG8ERihCKYisodXeohR3cBBcf3AbR0n6SURfxyRK0NcRA5vwxb1rkNzny3t3uXj+DKWS4YOP/4yNwRZSa+7du8eDtTUKK/MIa3DE9JQVRdFQ4kQRJzGCtALYE8MKYOuNKmmFEGOv3wzGAsmCkvG4sHySUrnGjjE0o4gvrl8hQdJv1XGSiNfPncbzS7ixRQh9kNblwNgDqJSiv7AIQKnV3Kdv8wQ2bcMXRRGRioiiEKUcwiTBGgdrBJVKIe0t3O/jOA460ZRKJfwgYDAYECfxUMcxv/nLyXlROXgIeDgvZb6sR+apQ99Jikc2kmkKHm+49NH9PDX22s1jz8tjxveV3qGLGcc0zuuctdq+l/Aox7TfvnKeD6wkUYJECgYioR62kcayoxOyDscGcJQLQmAkrLfXkSagM2jzcOcKmDaJ7RJKQ80vMFetUioUiKMIR2XGytiD7bouYRiitcZ1XJIoIRiGf0PjYIRCknbOsJ6P6zkkw4hD9tGxjG+EMokY0KhEMF8qEyvFIIn48OoX7DRbDKzBtQkPt7f4+LPLXPjJj3BcF6EfVRuYOj0T3v9Jj+OgWkNLhdIJQbdDv1Te9zQHhYC5uTlY3yBMElq9PlYpVk+cYK1dZ3F5Ad91MCZhEA3QOqbXHyClohuGPNzeYaATlFNg5DLNycl54TiwAehO3ChaOw7ZJtgpmZPJ6rxJJu+6JQpv2EUgzXOZDO0mGDEWTd2dN7PX9vYPac7KGLdMbO4YwqCKPa0RC9hkJJkyPVbB7N0avvq7cwl4jz5tLR4Cu0f3eIMgmakoMZ2/dRCUEKPEeizoiXZfX/XZyZlGWoXUYvrCWBjgMJZigrE61DCnT4ZENkIkFms1Qig8o6naiJPlgILVyFjjSIW0AtdzcRwnLbIQgjAM04panWAEBDI1AHv4WCURjsJTDtJa4kFIEoaYKMZECSZKSIYyMlmY1fd9rB2AB17RYxC1+HLjHr++c52OcNDKBQE9HSI21/luu0NteQEVSpTQw9BtPPImaq0hjhFxGrLNKoDT82FBSnrVGpXGDqVmg7Yf4DhOGjI2abpMVpwSJ3201iwsLVHY2GQQG74c9Hjn5VdwqgsMrt/indffZnEhodvZJAwH+J5HfbtDuTbPTePwpw/WaQhLonujy2TyO6mcnBeOg4eAJx5NOoDEPhp+s7QDp7doJx7ZKYmHY9EHnDGxCUz6mjiOMGi2j72Mzf3GerB1vhpm/yCMr+Be1d9wXMeUGQmTn4+x2mPO84Yg1bDbfXHM8NXJf6a8/GKsEYqQYAUSWJqv8torL+NKgbDDecJOS1IxzIvLDC5j41EFcM/6I2+1EAJhU43PVGNPY7R+pBtGlp9XLJZIQoGVFsd12Kxvk1iLFmClBMcl1gmNbpftZhP/7GlUorFa753nOyHwnOUGTtKtzaUGYKsOKyf2Pr+jeRE8x+HUwhJ3HjzEFYp+p0ettEjSDfn3fvRjarWQzYebfP7pVebKC9TKCzihpuVqPr52jVDrNCxuDPt913Nycr65HF8nkJycnJzjQgguXbpItVoZpxzsQVYIEQTBSA8wawHXs49KwMBYBiZJkkciCVklb7VawXM9lFQsLy8TDsLxBqwlFeMTxMZy5+49kqEhuZ9uZSY9kyTJIwZitzYPpIUgB0EKwXKphBdGRFvb7Ny6TevefZY8jzm3wPrdbT7+4HPu3l7nyxv3Wa932e5FfPTJZXq9Ho7j5Pm/OTkvOEfqBLLbM7fXnDdZzQZM9b/caxujf4XADP/OJBv2Wmdye0ctTDluntV+8yKOnG8+goX5BZJEp+LNwzAoIjXgJkOonufRbDZwXZc40SMJmI5xQYIYzhWTLdb0hKcuiqLRXl3XHf4tMMbiFzwunj2HxaS+aJFWFE/m7a5tbhEOBvjGIi0YY5HSTlUYCynAiNFzswzAYquBnZjzkiTBDXx83yfsjkO21hikjlFGc2Z1mblSQOBAbXGOne0dPrt6m/VGn82Bptfc4vJGk81Wi7/cXKPdbecC6zk5OYeXgXkkvJHlaO2aT3aHgKdkYGbJuWARUpI1z9g90U8mUE/+OzmhHkUe5jh4lsbf5L503tA95xuIkpL5+XmklOgwRjhq4nttpr4HeuiNs9biCo03rMbtGReH1FuW3ZBmLeAy71/mDZxk9JzxObG0RCglg/5gaAAyNAAZzXm9fp9Wu02tXMZqC9Zg7ViuZrQM95ctkwzKFYyUOElCcdAnHsrbZO+bzBuEVFqqMF/i1Eun+Pb336JSqeGVfR6sPeQX73/Aje11tjtdHjaatBPDvZ06kYW2SZ4LKaqcnJyvnkNXAT/yt5jx+pNgZzz+OvNNOY6cnGeA67lceukShUYXu76153smvXqZvEuF1Ps3sC5mIsNFKQex6/3Zsnt7KYKi53Lq1Cl++cUVBv3BzLFGYcjGxgYXazWsnvFVt6C1mfI+Tr08LAQpN+pUux3qc/NorVE4GGvQxk5I1KT9xR+0N/n1revoUoG33nqH+59+RBgmuCtzfPrZJ9xcX6ceR3S0JXYUCInIyz1ycnKGHDwELNPJJ9Vw1lhhRx14pxTxhvOanfgPxhV2ux9P7UMIxESFsbQCY7MU8untHXjc+3b1yBLWxS7lkmkP5Xjc45v/Q40B9lwp2+eT2IZPsyPKUTnKOcp5MXg0XWP6+cyzV/RcTi2t4IoWnWYHEUfDAjGJlRKrJChJbDWx0aTFx4JgaAB2jQdWkvXqFlKl3j9tSWzaZjcxoC2YYYFJZgBamxpb5cV5/Pk5rj14SNdINAJlLIYYIxIslgRBKCSbnT6hMXhSYm3yqPcvk8EZeiGTJMFxnFEqi7WWbnWOcqNOqVVn59SZdBzWYrXBCglSYqRAS0toNJ9fv8vVu3VC8ym3H27w4x//mB+/+5uEToF//O9+zsZgQCQEsRAgFDhO6j3NowY5OTkcRgbGSROq065GMXvWe1qme2sqsEP5k8nKt1kVwtZaMIxqPVMJhMwYs1hpR1W7u/MLJ7eRvbaXPMz4ByiVjM0eZQYugDYxo9rFYWL48PBS6Zg9xrAfckahnUVgpTr09qa2vUsa56s2BoUQSPFkx5TzzWX8+RzfVGUYY4baeJJqUMBPLEpbPBRWyPSz7ihwFcJzkIGHloIIg5agEQRmAAp6JgAcsA7WOsRaEoaGMIHEKBIrSaxE2zQ0nBmASimKxSJLy8usvHyRhqt4/94adVEgIsE1A4SJGDhghGAgXNooHvYjQikRWiMmcv9GOYBGIN10uo3jmMFgQLlcxnXdkQHYqc6xChQb9dT75zgYbTCJRjpp+NkoSSShZzTNeof5YsCrF1/izW+/yW9+510KvkeoDSiZ3qAbi2Ml2lhspKfayuXk5LzYHKIX8O5Yr5h6Nn1l3Ov3ScScJ7dxHNubtZe9Od797BduyafinJy9cR0H10mLOqYLxtJwbiainBlZURSRJAklJ/MABkx++7TWJBNh3yRJF62nvf0AQRCwsLBAUCzwoL7D2sMNEjNW5lSAMOlgLIpYC+5vbBHMzxNvdPdSzjwQ3eocAOUZHUEmn5FS8MrFi5QqFc6fP8+JpRWEsQSeR8GMbwpHkk0WbB77zcnJmeDQMjC7vWqHWe/rwtdnpDk530wCz0U5atqrLdL/KTVuozYYDEiSJK0UFoKizCqAp82wyZy/bNmrGMPzPIIgwPd9tLXcXbvPnYf3idEYQA8XaUEYBTgk0mW7N2A7jCjMzTE5g4yVCh49xt3FGL1yBSMkThLj93swDB3vVeCmlMMrly5xavUEpaCA0BahDb5yiSYla3JycnJmcOgqYEj7amZMTtBTMi8C0qkyZXd17n7yLpPrZO+zIs3X2et9R+kYsl8lnJByTyPQCiZC0vuPYbLbBxM9N7/qEO1ezDpfOTnPiizVwlqLkgrlplWwUkqsMThKIYVESDFSB8j+zXLqXBvjCoOx0E0clDvefvb9zHLwslw/sSth1XEcSqUSnucRSfj0yuf04hCrAqxO0KTeNGkFAgeLQwysd7tcefCA5YsnEVJM5RNaa5HGIEX6b7Zf13VH86eUEmMt3UqFSqtJsbFDo1RGRzFy6OkUQuD7Pv1+H0cpHG0QSEwYQ6IpuD5xGGN1/h3Oycl5PAf2AE4aT5NK/DOXGevv3sas9xz2fbP2c9D1pt434xiPMoZMIeeontOnzUHOR07Os2D0GRTgDIsjEp2M7TPB0HDyRnm5nU4HSGVSCvQB6BtvqgIYpj2A+938ZZ1AlFIk1vCrjz8mwqa5zTKtKzEibWcpcQAHbSUhkg+uXqMwt5B22BBi176mNVCzsQghkFKN5stOpQakYeB0rb1vLAWgLEhtUMbiWFAm9QLu1aEnJycnZzdPvxOIfXQ52gQ16iH16PKseaIx7FkNMn529/b22tfz50QcY3f9mz3+Oh9TzjPHdVJvoE4SJj8cQkh838NRaRSiUCgwGAzodrsURSrV0jWPZuFNev728nKnsn4CRznDHEOHO/fvc+P2HSKTjOUOhjqAEkXasE6CVCRIvrx3n24U4jjuyADcvY/J8WithwbnuACtk+UBtpuPPUfSjhdMmi8tEEhrcxMwJyfnsRyiCGTMZMgQ2DsEjMU1Ltm0Z9LyYQA0JpV3yW74Z4goT3kCrQC8sX1hzEQ5isbIcYj1IOHgydcOKowqbHrXnR2hMWPLZSQjs+fMqyZm//F5EBjUZEgZOVpfW0YdUR5lbxmH/Tx5B65Ylo+/J0i9Enu/lnkmxu/NjimV37B7ViFaJqV3ngfkxL3R4cWHYLLK/FGev+N9VkylRuzyio0qZ7VGSQgcTY8Y5YB0FXgeOC6uUDgWdD+k3WziCYV1PMqkBmA7cUmSBKXUyOCbzPmbXBIsVincBDw8fMq4zjwUlvjZz96jbjwSmyCswVoDSKx1iRAYEqCffp8ltGPJ+1du89sr83Qb65SUhxnEmIImkg6uHs81QgiiKMJ1XVzXw7qpPmGnOvYA6jjG2rTVnZQS3/dxXTcNT0fx8JxZrJJYRxJKi1QQS9DGMDHl5uTk5DzCoQ3AvcKde/5tQSHHP51TBpvYc/39WroBSOuk27Ng7MQPiLCICTfa7o4hk9ub9dpBc/OkHe7VgjAThkHmHXiETC1xtCMmf/iEyQzhab+oQWZuiT0wU9sYbWuG8XfQYztwGNimEjaz3HZi4qRMXOlUy3HmMT1fjM+FxR65fHKWMf3i6rBNntfdjHL+sCgJjtBYE6VpFFKAlAhHoYRAWkjiBJmFPbWh5KbFD+1kOrfuEU2+KX2+9EZLSoeiV2auuoTyStQHmo+u36UeJiQWlNWI0fdIDq+gQQqTjs1CP9J8eu0Wv/vSRcz6DhJFksRYY7HCIq1JcwGHY8rGqJQc3XilhSACN4lx+z3CcmnkuczyJCcFoQ0GhMAoQSIhlhYzSmv8mnzZcnJyvhKeegg4C0tkj49je3s9fpaIGY/3X2PSQkwfi12vPGpD7v/q88g38ZiOxuPPRM5sPJXerCVJMuXGEkKMwr/THTUsJZn29O1ob2TgOY6zv/d7GEJ1lcLzfcrzc2hHcb++yWfXrmKGOXV2GFYdfWuz5N7hc1lP4au3brHd66KKJWIrsEikVUijDjRfWanolisAVA4QBh4zLm7LP2Y5OTkH4cAGoJRytOzmIEUhUsqhSHAq1yDk7PfMYvf7su2JGdvbbzkOxmM4uCk6ve98ps7J2YtyuTj24ik59b3Nqmyzfr7WWnxinGEFcE+7U16+LP8ve+9kFEAJgTKpEac8l0haSicW+eWnH9Hod0c9eGd50bOCDmPTba83m1y+cxenWmNgAeGCUYhYgmEqHJ31JU5Dwox0DUeFIO3myE+avR+YmoetTV8zWoO1SDHdGSifYXJycmZx4BBwJv3yuC4cez3O/hZCDMWd5VS+2+737CXVsvv50QSIRQqZxh3F48Odk/mKTyJ5MjUe0ny+gyTcPHJ8Rx5BTs43l/n5uZG+n1JpyzSkRAqZdseQZmQEaq2pyjT/r2e8dIYZ3iTGcYwxhjiOR0ZgZggKIVBIMBYlJF4QUJir0LExf/XFZSKdto5jn5vGcVVumrvYR/OrK1f5zqtvElqFYwUyASsFVhiMtKOOJ1lun5ISIdLqY6310AC8S6XVGkXKkyRhMBjg+/6ohVyW0pIdFxMdSNRwfhRCkjf/yMnJ2YunXwWck5OTc0iCICCKYwSPVtPuRVGMO4A4jjNaYHeoeBoBeEpRKZWYX1zAKQbc2ljj05u3Merw/jPhenx2+x5rjSZOsYRFIaxCmumu6ZknMF2mb0QnPYBHqeJQSuF7HkLIIxYw5eTkvAgc3AC0AqxAIMfyB0xPavsxeac8UrKyuxbkofP6pjLp7NFyro4tLPscx1uOX+dvj+tnRS7pknMsBJ6HiRMsFm1TA1AwlDzhUU9/aegB7NvgkXSV/QxAhEC6ikKxSK1axXF9PvniCh09INTmUO3TrIBeFLLd7XD15k2KlUqaK4hNC0iGVblp2NaitSFJDFobLAIrBBZBu1zFCIEXR3iDAdaK4XrTc5wZGnd2eD6ssUgEjlS4not8juejnJycr54Dh4CFCIaPLK7jje4rrY0xNh4+nh0ezl5PtyCGFcIpxuhs0wg5DKfusb1Z8i7ps+O/xxOfxdiQWRbJrPFNVRjvs87uLiNywsh63jpqHIdEzBRWsbfMiQGSw28vJ2eIsFBTAV5i2Q5DXCkwAgINBS1wpCQMw6m8vswD2EqcUepIHMejitmsWwhMf4e1siRFF79couiWCBOXTz+/xVZoMGLvjkD7oQX0peGz2zf59956C+FoBBGaBBN76QSXHiWOo9CJYNDXFIoeSEmCJhKGbqlCpdOiUG/SdUtkqlnaVRitSLQgArSCSMf0+33CXo+l+Xm6Roy8n0KIPASck5OzJwfvBJL57mxWCzf21gkO3tWDCR2waU2wvbtmPLbbh5j2/mVjEdnzs45nj4KQrHPHYdZ55Awdu6ftyXk6xTCzqlyfv+PP+fqhhi3grDEINcxns+myu6sHWAojEWh/tI3JG8bJkOtkrrEQAtdz8UsFVOCx0W7x+dWrWGOO6MkWSNfj5v37NAZ98D0irUlThDNZlul5b6QkKsbfoVZlDoBqpzV+nwWdaEDgqLRXMkKQ6FRDUCd6VLXse96BND1zcnJeXJ75DJGbBzk5OY9DSjmqlt1daLbbAPRtiBIWbQU97ewZ7p3VCk4Avufhl4okvsOVW9fZbO4grUCqWULe+yGII0O92+fXX35J4nqE2iKMxJqDi863ymkeYLXdeOTYAZSjplrOZUuSJCDA8/x9q5dzcnJyDhwCngqbjJKyUxkWS1ZxNm3e7Q4tZpORsgItzNRz0+vsXfk7KxycSsM8WjmMIK0Q3tVx4JH37XOsB52wjxshxyHlr2oMOTlfFVmF66R4e8aklIq1lmDYA7hrvFF3nmzu2N0CLjMAsxxBazTVShW/WqKhB/zlZx/TM2moeChdfriBG4kxhoEDf/HpZd59+SWk8lOZFmkxwkzJXWmtJzqCuDiOQxRFtDMPYLsBmdTMsMJXSvlIcUgURQwGg+EcKXB9H601SkmSJL/tzsnJeZRD5ACOjZHdXRIEY1HUyfZqe3biGK43uc7ktiddhLO6jDzSMSQLRg/fPnl3zx77ydivMpAZ+3pWzDqmnJwXATH+0j4Sis2+CtlNYUGmBmAn8SbeMy33NKkLOPm6qxx818UoSd+xfH7/NuHQiDp0hsSoYsQhspZ7jQYdC1UvwE8MnaQ7maA8pVOYGXfZHNUu1zBC4McRfjQg9Auj96aePabOSyZEnSRJqikoVapNOLMdYU5OzovO8YWA7cS/B7VV7PTjJ75PPcoYjptdx/TUxmJnLM89hxv0Xu/6WhxmzpDZH9QDfQIec7G11hRFZgC6hx6dGmoLKt/lwc4WNx7cQwuLFaReu0OQ6hgohHSJkWx1u1y+eZOgWMFqgZQHN8aMUnSLZWA6DLwfURQRRVHq3VQSrJiKqAD5lycnJ2fEgQ1AhUAhcIRM1fOHlbxSKFJHooPAQQgXIVykmJ6MJ7tmSCFQltHiWDFcO932QQoUxrIyY3mIRxZDWq06XLK9pONUe3YWEWIobzOSvVHTy8Q64+4mQ2Gc4X6VHZ8vte9xCNJK2uFi5XjhUe/n+LzIGcuj3pKnxUEKSay1GGuGXRIMYEAM/51a9h50Jm+hrUkXLHpirZyvC48agJmAiZn4yIrh98gRAl8qTBgjGX6OsFgJRgqM0aMuGkmSjFrAtRJnqshjMl9wUvx5lIqiFK7r4AcOTrHEB59/yWa7jzWgEg3aHPKm1IJMQA4wNmGQWC5fvQ6FgHbcGxWzTHois7B0VtGcCl+n80yrPAdAudUYeQmzNBilFEJOby+rdFZWMFcqYwUkwpIMpzN1HDfZOTk53xgOHAJWmXyBTTt52FFo0hlJG1gLMsvtwwIRI5GWybw6bUd6XuPqt3QtM9EhJJu0dzNVVcwuORY7bU6ICbmZySJka/XQQmSqswiA1lm13vQ6oy2LcYh7ZACZyU4gaY10ugk70uva6zgE7nAMYCf9ITYh0294tDvKxFimMMDhvBZHYbfhN+s6AaMjT3/yZxt7szBYdB7+/pqz93W3Yni/k904CYEjBJ5UFJSLCWOc9MuGsRYjBValVa9xHBNFETqJKanUAGzHLlrrqXkhiiLiOB61XANGN25KKQoFn+p8lVBb3vvkKr0oHe3wW8nEVPB4BFji4W2KwljF7QfrrDV2WKp6JEmU5kzvqk6e7GiiVFrckSQJrUqN0+t3qbYbU0beZDcQeDSULIGl2nxaITw8v0Knk722YHIrMCcnh0PJwEzPg2Lmq5ksy8G2d1ziIXuMYMZeDrq3w6/zZGtMPncQjvPsPWOOckmOehlzng8Ocu2Ghpvv+DhKHejyFlWMFJBYQd+MQ6yZ9y8zrLLH2d/ZDVyhWKRQqrDVaHH1xi0MCoNzZA9zJlWTevFdtjptfn3lC5xq5dC3ZmMpmOZj3zvpSWQkA5N/QXJycmZz6BzAKd08Jr1ju984kci9zzb2WG3vfe23zozXnpfp7zmUBvx6kZ+/bz5ifJld191XgmXy+14ehn87iTtVHDIZ/p2sAjbGEIYhURShlKJYrmCkw8Z2g3q7i8ZBo9BIDhsAxoKwAmUEWAeDQ2gVn9+5Sxcww/DzbiWC3Ut2fJ1yFQsEUYgfDsa7sak/fRzNsFMGoLUWL+sEknvPc3JyZnDgELDrTuf0ZRW70ophfldKJhEjGMosDJ/fLQkzq6OGlJPh5T3CRhO5M5PsDguPxmOfTbbYXmHRjOdBVubrihBpHuWsz1HON4Qs1GoZt3E7wKUuDlvAteI0bDopsZJJwIRhONVPWGuN67okSYLnB+AE/OJXH9Lux2gcbJaneoSvqUQgbJpnbHGIBdzc3GI7jlnA4iFIkgTXdUefZWstYRjieR6u66KUSkPXQtIplqn0OlQ7TQYLKxhjiON4eJ7USOomCyV3ez3KYUixUBzmOevcBszJydmTg4eA9+iaMc6T26Nzx/DlWYUC+xUQ7NcJZL9t7dc95GkzuygiPRFfxZi+GYzPX04OMBXWrTipZl8rTu9lD3JzJUTaKi0IAlwvoBdpvvjyOgNtscIBkXrvrJCH6gUMadRDpeVyIFJvYiOK+avLn1OsVDHG4DjTYtWTmoC754mRIPQBwsAAWIuSEs9PQ8C58ZeTkzOLJ5KB2TcNyzLM8mYo8TJ+vH9Mb9Y6T8As5ZEZ43mssTFaRzy6zeNm17jFjOe/PjIwOTkHY1YEYLLzRcVJ+5C3DyIBY21axDY0AP0gwC+VuLO2wfXb9xBCMb5zldgjJJqm5V/jinzhOITacuX2bRKbFjSNDMBhKDe7UU41/GYYgO1HDcA95wJAKYnvuii5u5dxPkHk5OSMOVInEBhPzsJopBlX/mYVZlYIrPUn6l/HlaLCaoyN9t6RBWmy6jaDyR4LS2SjQ+eDyRlznrESw/hHQ4qJUyHjtEqY3SFbAZPCqnYYs0q3yJNOsGNpm6HXa1YIbMaurDhahd+scPzzyNdprDmHJY0cKEelIdqwPfKKTXbBGEmmYCkNPYDt2J2qhgXG0inWkFiD0hZXDyvSjUVVizSCAv/qvSvc6ScYJRBJmObhHakMRJBg0VgsIUJE2ESgkVy5t82NZosLpSIm0RAOENJi/VTzTxuFJ7xRqNpxHIwxNCc8gNk50FqnVcDaIGODTAzCJhDFECUw6FF1Na61SONghAISjNDDaSP3pufk5BwxBDylgze65310Sf+fKuJhs8fp3bFg75CtsBPrTz0+2gHOKh5NvYupfp5ADvNl1FAHcL9w7oRO4OjvJy9JnQwhZxH0PZX+Mo3D3c8fw36fd75OY805OgIxkkvZ3bkje2ytpTSsAI6NYDC6aRwXU4w9hcObz2GFricdSsUipVoVgoAPrt5gYCAxCWKi/OPQ32qR3vgaAUIYBBpsgkXQjzWXb96EYoFBnCCsGHokU43M3a3qss94s1TBAoVogBeF410JMaw4tghrsdpgEo1NNFbH+Ao8IYb7yVJyshLlnJycnOPsBJKTk5PzDCk7Q/2/xGW3qTZtACZTuXBCCHzfJwgCdnZ2+PLLL9FGz1QtOA6shY+/vEEfGFjQQgESYV2EUehkXK08iVYO3UIJgFq3NXP704LSqZ6g7x44wJOTk/MCcmgP4HTXDDFSrldKPdJRI/MSTnbNyJ4X8tHtTb9v12tiovPGrvfNGuvu5ZH3ycd7lPbbxuSx7tZ6GY/76+Wtep69bJNit3kl9TeMoec7qwZOu8eMxdazOWbSsMs6gDQjZxT2Tfvk2lEFcLZYLFonmOG6fpCKKX/22Wfs7OwghXyqn3kL3K83ub6xBUERjYPRChuBjeyou0kURaNQb0YWBq51UgMwM/Z29zjOno/jZDgvO7kMTE5OzkwObABOtT6bMOqyiXnSCEwXgZzx/OMMur329TiDMWO3wbbbAJ1449QYZrHffnYf057rfI3EWPc9X88Jkz94Od80suSMsbTJpAEopZxqiVYedgBpxWqcXyzGEjCZQaR12qXHGDvy/lXKFVzX5VfvvUe/3x8ZVE/v0AQ94P0r15ClCrGVWKOwscDEFmuYCgFPznPtoSB0rducCm9P3hBlZNuQcnhT+hx+h3Nycp4P8hBwTk7O15KKm1YAZxIw+5EZVZ7nUSqXqdVq1Ot1Pr9y/WkPM90/EAn49MZtWmGEkQprFdKqUdHbLHZ7AB+HlKnB7Cj5tYtA5OTkPDsO7gHMSjrEsDBjWIwgYM+KVDEWQxgWNIxqW5ET5QtZgUhakJHJMMwYgwVhGO/bDJdDHPB4fLNfOW7P12T+tbTjx+KI0i1ZXndW9TtZFDy5r8PuZ3dHgv1e++Z54Czpmdy9HFVfZ7/tHXALT3y+9xnD81IMYCc+s1iktThKgYDEGowAbe3w8y5GZ1BiKKk0X64VqUfOUeYdS5+zCCxKgON6uEERr1Tjyq0HNNq99Ao/g8+zFZZGt8P9hw/xggAjDEYajEzlYHZ79LJ5qF2uAlAI+7hxNHp+0us59VkxBiUsrqOGBqDgqHXNOTk531wOnCUcyKH8iYWEBDv8AQmtxTCeiCZlOlzDSAZGCIMZqqoa4Qx1ttINahtP7MnAHl0zhQVlIOsWYCaE+o0QaPn4bhGTuW2W6S4hUzI3VmLtdFXhkRmOe7yFcVjGAPFhO5UIQE13WMmGJxmeoz3Q8mCmx35hsFndTZ4lT1cGJmHvuxl7BBvQDrd3dI7F0BYzjuk5QVhQFqRJjT+JZK5cBinpRiFSSmId46oCxhEksQYsZRUjBIRa0E8AzEjfTwhBFEWEYZjm/xmDKwErkY5LobpCyxb5009vUdfDjhzPxFGWhrA/+ewzvvN7q4QqxIoI6UhU7BDHMdZalFKUy2UcJ30ucVy6hRKlfpdap0m7XJkKWWfSONZa4jgm7HWg6FL0MpkrQSpKrdOq4WdxqDk5Oc89By8CGf43+TdTzwyf31VAMFrPjteZfGVanOVxY5h+l9j1yuM8d48v5hCjlJnjLoSYlJ8Rj5zNw23JZkakEHvqah/urH59eCYFKntpBj3HBtTjsXsf03Pw4XjcEDKRZDu6VRyTVQC3YgelnFGO4CwMFqEkjucRlMvExvLJlS9InqGWpDXpPHjz/gM22k1kEBDGCZNnYTK/b/KY9uoIMnlzMMqRHErKOCrVUswjwDk5ObM4HjfOASaZscjxE21maluP3V4++32j+OaGn3NSHp0jZl3rskqjBh3tjQrN9vu+GwFWSVTgEZSL3Lx/l+1G/UjC6UdFWIlGsd3t8umt21i/QKFYAS0nBPPZ0wBsDsPAtWFHkN03QlnhljWpAamUwnOdfA7MycmZyZE6gUz+CDvSIbs3z6QXHrs+Bm3snq9hx5Nh1gEgRYBQTPoBsjGYXXlas7qW5IbDk3GUsK+cuMcwwkz90B2FvPvHNxdHKjAG3wsIw3A0n3ieNyWUrLWm7KcewE7ijT6XWQrK7lw6qRRGaoTv4pWKaAmfX79KK+wTGjjENPhE2ESQWEHHWn519Ro/+cF30d0OKrb0wwGe5+F56fHEcTxSWdBa0yylBmC10xx3AplQYsgMRqUUcRzjA0IeMO8jJyfnheTAM9+eiccAdhjKFLty7CbU7Ce3ka2z57aHImBZcHRyGzbLXRkmPE9vzz6yrd1jnfw75/AcxZMwdS3yX6KcxzGRfjHp7Z2UdkkNPKiocQ/g3Z/NJElG+XQjHIn0XNxiAVXw+eiLzzBSYI9a43NYrACrMEIQIrlbr3N/p8EJ4SFMgkCMwr/GGKIomjLy2tV5AIphHyeOEK47Nd9OaiVKla5TLpcZCSvm5OTk7OL4MvntxL9HnFAPNE1NJr09q8n7OWOUlrZPFfZzjZ2x5Lxg7HXhH/0gZF49nQwFjjEU1LAHcDJ9D5sVQkTRdK9xoSSO7xKUinT6fW7cvUNsdscOjoO9P9xpvmNaXJZISTdJ+OWHv6ZSmQcrUU7qxcuKOaIoIhker+M4WD+gGxQBqLYbj+w1M/4cx8FRDo5SLMzPDw/+WA8wJyfnG8KBPYBKqdHj6R6dIIeVrMbKCW+eIJ5I4M5EXMXwNTW63U+reNMb1XSaNMPXsnUAhJAYMx7D5Kyd3gmPK4cP6umTE5XDB11nLw/js0bYiTndjh2gR5nnn21O3X73G3lo98UilagRGLR00cLBEQJfJFT9GJUMiGMNRmOjBCNirKtRymXOS7/rAy2JrUpF54dh4DiOR90/Mi+gsYZQG1ZKRYSjuHnvAd3YEgkfLSXYR1UHjn5MmfTOrlcE2KFmlTQWheTqjbv0f7dITxVQURc1FJRPkgQhBGEYjoxfgHZljtKgR7lZp7l8Etd18Txv9P5srtRRgu0nnF5dwnMtvWSAtWIotZV/z3JyclIO7AHc3dFjdMcpJIp0cRCjx4q0DdqeHUSGmoKpnmC6jhTp85Mt5Ka7cGTagWIYQh6rDO7uLHIgxERbugOuM6sryLMm00HcvUwZhofg2RRXDK+bHWtAjpfcRfGikX5zLQiNFQIrFBqBEoaKD8okkGhIDDZOH6dSMVBxsvCvN/oeZt/JLG8wyxXMtAB9z0U5EjcocP3OXdqDhNBIrHL3GeVRGMpYiT0WFYOIkRikFXR7ER/fvIuaW8RMfAez8cdxPOoNbK2lVUkrgSvtBtba1NvnOKPXMwPQxhaVWFbnq3jKIEQyHEP+XcvJyRlzLCHgaXGXg00xsyVdDrLm4df6prCXQslzoupxQJ4zLZKc54Phx0Apge86+xYcFWUIQCeZNt6yIpEkSaaKhYQQ1IplPNcFR/H5retohiLRyVPwiM36SA8dhKlCpCW0mve/+ARR8NIw74Txlx1HFgZWSo0KQWrd/TuCCGsR2rA0N0fB9cb647nzLycnZ4JDG4AH1WF7RA/wII8PqI03lfz8lI2I4/D0HamA4tDbFuSiXzlfZ6w1SCHwPe+Rft6THq5SZgAab7RuVvWbec0mvdpKSgLp4Hs+najPzYcPEK6LRCIO15zlCQ9wvC8NRFJwc3ON7X5nNITM6MsMwUxVQUo50gIsDXo4cTQVjcnOgbWWJE5IBhELlSpLtSrKknZMskcRNM/JyfmmciQZmMm8lMnQYTYhQRrtk2mJ7iPr7/W3tUNX1j4TVCZ3sHsd85RntSfpPjFZpQeHyLkTWTO9/SVsRsawyLZ9qOF97Xi6nUByvkqMMeBIarUKdphXvDvMKaWkRGoA9m0wqhjWWhOGIYPBYOQ1i6IIpRSB71NUHp5yeLC1yVq9TjsCRWHkkXsWyImPqxbQQxN2WtzaWmPZVSRxNDJeM0aFHY6D9gN6foFi2KfaadJeOUUcx3ieRxiGY8PXGIg1xWKBc6dO89ndNYwFhi31cnJycuAwnUD28OhNGjd7PT7s+w46jsOu8yQcR/eJo2xDwLDZx/7rjLZ95NF9fXgmnUByviIEUjp4jmJ5eXnqRmZS4sRX4IlMBNofCUBnYdPJZbxlQaAcFubm+PVnl+kkMTq9Z0LxbMjyc+XQ4DQSQpPQMxGf3fiSQqEATFQ8a00URQwGA6IoGsnhZHmA5T0qgTM8x0Vqg68cXjp3Bk/ItN0eecJFTk7OmCcOAc+aUEYZXsPiBIkYPd7XXLFivLDr8TGy39YeaVE3Ct08Zgyz5E2+YV65qcbzB3Y57tYJOvzJeZadQPKuI0fh4Nd2/GnIUjksSgmk61GqzBEZQSIctHCwykE4HsJxCYbh34F10UIhlAQp0MYQJTGxTtBGY4wefYMlAke6aC24cvMeVqV6fOneZ41xvy/z0b7oYuJlA2k/dUdx9dZNdrpdlO8jlYMUAmsMcRwRRgOiOCQxCVZAcxgGrrQaaeRDptOjwQ63KYniBKkcbGJ461uvU3Bc5HB8uR5nTk5OxoFDwMFEtZwy4/BrLMSon2aWg5LhmfGUKIUctV1KZCoZkzEZypPSY5woo9EjiQaTyjUc0g6c1RVk+Nee6wjhIhiHnWyWPW0tafbOHljJbH/CcclMPD8cKfwqhhWSz3q/RyC97vmP5eHQ7P0F3fs8GitBKIQxOCS88tLL/OFPf5/auddR7TaytE3Y7xLphKKvcIplrFkDoEuAKgVEUYR0FXEc0ux3GURdtE4Q2qCswHcUJddDuxXu7Giu3GnQ12UiEnAGYC3CODNuSjWHr5yY/ZmZbDsnMnsx1mw2O1wOB7xUrRLEBieMwSYYaYhtTDOKMZGmVKpRrw49gK06fTlAVVyEdrFYjB8QOz5RUGAziSiECS/NL3NpYYEP1taIPQMa5LPsf5eTk/PccvAcwF0dObIuHIK9Q7np+8bTauYFTNcZetnEXrltk2tN3lUf/sd4r04kGbM9O2JYS5Edh2HsKDXsnaiYPbeXQzU3IsY8gWDhMyY3/o7CYXSI0u+M47iYOOHMiRP8T/7Of8RibY4vrlynLARlz2H55FlKvoON+lgd4Q/SCtidSNEa5vhJIUmEQyIEeqgjKmwqMwUSIRXB/Dx//uvPafQGJHhZVQTY/Qw8O+22exLEXn+m0QVjDZ/fus23fuvH0E9ItMXJNEq1xUYJxBrHCAon5uBjKPe7/LXyAy4nJ2i6PkYaHL9IsbJAsVwEByQu5aDIqy+9wuXtTUJhnmnv45ycnOeb4+sEkpOTk3NARgkW2lLyA/7wb/4BpaDAoNOjXCgipEOj3WOr3qLdC/FMxI/X/h3nBncBWIr7+IUq1imiVYFY+iQ4aKEwYvgvCi0UiVD0hObTm9cIrR1HFczx2XdPhIAHDzeJrSCxDtIN0DgYHKwWiEhABGUS3jq5gy2l1c+lZpvvVR6yUiuxevIC1bkTtDoxN+6s0Q5jCtU5/KDM26+/jWMl0khEXgWSk5Mz5FBC0LtFVzMFfjnMC8wStR9buEC67m5hZSnlVChmtxTE5Pumtpetu+t9ebFATs5zigVHObhCcPHsed598y069QatnTpxPyRKLMXqAt0wITbw3Tt/TLH9ENw0zWKuvcmbnRsUFk/iVpeQpXlKC6ugfLR0GCQWLV2s8nAKJTYHXa5trGE8B6vkaAziGJ18R8UA7cGABxvbBOUaoRHEVpIIRagFEQqjPJbLfcAiFtOWcGKzA8DZUoh0q4TaZac54N5mgy9u3+GLG7dotrpcPHOJ+WKNAgHim5eNkpOTc0QObABOdr8YG1xDQ2u3ETeUZpiJYKoLx+R6e+13r/dNh5155LWvultHTk7O/ggLrpT85tvvUC2USPohNkpI+iFCuuD4hFqgOg3Kva1Ux06Q5u3FmpOtWzQHUO8b1ht9guoi3//xT6jML6P8EjgBRnpUF1f45OYNHnZbdONwFI6VFtRz4AG0VtANE27efYBVLtIroIVDZCR9qei6Dk0p0NlsPRfAWhvefwBC4DoeCQGD2MGIIrglYscDLyAoVjl/9hI/+s4PcfRBlVZzcnJeBPIQcE5OzvFxiGJZAZSCAhfPnsd3XQLXQxqLJx2k8oiNIChXsVkfcm3g6jbc2Bnp9z3caVHvhrilGgurpwi14fVvv4XjBRgh8YIiRijeu/wFXa0xcpyn+Nz0oRHQC2Ou3bxNN0yQXgEjFDj/f/b+7MeSZN/zhT5m5sMaY44cKitr2vN05qG7T9PddEOjS0NfQEJCAonmAo+8NBL8AQgkkHjhAdTiASRekBB9W5cG1LfvpXX6nj5Dn73POfvU3rtq76rKqqycImOONftgZjyYm7uvFSsiIzIjZ/+WvHLFWu7mZubmZj/7Dd9fhFjpY7fWCG9eY7d9AymAaz04mMLnB8hcs5NsYolAxKioS399m87KOsNJQpaDIuTv/v7f5d3td1HIuefwBpMVNGjQ4Am4lAZwURvntH9i/u8Fnr9FjZwUT9iF1iPlztHgleW+Ivkt3zaOuretvS8GFoqMF4sR9a8HluV5PvuwVnD9+k1kEPDg0UOsFKQ6dz+bDGE1/W6HUbjKcbzhyIzBBUYguNP5gFmmyS20e6s8OB7w2d4RMxmzunUdIwOilT4Pjg/54sFjZhmoIHRsAtaAlZgXxgR4DoRAK8vO4IjDyZBMSqYqIN6+xke/9ft863f/LmsffpdHK9/kj7MPse+sQDeCRHPv7gbD3jdQnZhMamwoyHTOaDBmOJ5yNBqS2pwffP+7/E/++/8D3tncIpQStcAd+rqNtAYNGjw7LiwABkFQ+vjVhUEp5PzfZxylECglQp4v2NUzijzpPC+AvmzU6/2kur/uWGzrq9D/bwqMNa8x/+AlBEArMQhuffA+7dUeozQhxSDbMbkSmHxKP5JcW19hc2uLP//Of5eEqLzNr6J3+VfyO0ipGA7HDAZDjnLDsNVlb5LRWd1GRm261zb5yRe/ZPckweQBZpYj8gxhDZZXQwA0GHQIozzlF1/fIWmFXPvOt7n9m7+F6Vxjfxjw1cMpn9094l/vbfF/mvxd7nz0bQCSXx5zMDwmVxobGUyYIpUhtCHtTpeD0YBhOmaWDPmv/4O/w3/1b/+XaElJYK0jhrbPhWa1QYMGrwHebEmlQYMGLxDi4oeATqfLd7/7XdY3NsiyjDR1GT5ynROFEVrnHB0dMRmPOcwDhrILwB/d/Af85+t/QGIE0+kUpRRRHGMMZIlGqYjN7eu0+itMjeXPfvYLTEk+Vcse7pyHeRVyaFsDqbH8/IsvyIOA7uo6Uoak05TxYMTJ4THpdEYoA47HGb/68AcAbH7yMbnWGGMIwoBWu8Xq6ip5ljOZTBmNR0glGQwG3L17l7//d/8ev/2j32S120fh/CBfhUCYBg0avHhcOhXcnJm3/N/y85f9Xf5/yXXnG4df/iTdoEGDZ0NFsC1YX1/n3XffRUlFFEUlx6gxhjzPEULQ7/fpdru04oiN7ASAnWCNJEkQQhBFEVtbW9x+913ee+c2m6tbSBERxB1Wrl3nz372cx6enJCbV1fCEVaAERghuH9yzF9/eQfV6dJt99FpTjaZIXJNJBTZdMZap8e9D50GcP3RfdqTMe12mziOGY1GJEnCjRs3GI/HHBwc8OWXX5bp8la6Pf6n/+Q/Ynt9k1YQEQgIrBcEX90+atCgwdXjwkTQQRDMUbB4E5XWYMgBJ+QZY1CqMqv48+pmUWVBFcmJYCGDiDGw5Bpb/NSgQYPXF0IKwiCk1Wrxd//O3+EP/uBvEUjJjydTRG4YnwzodjrEKmRrawtw80M42CUwOblQ7GQKKUFrTRAEjEYjprMZqYU8z9neukZmBDZq8ycff8xY8IoTIAtsLtHAxMJPfvUZv/Wbu5wcjgiiFnqauAjpQNHt9ohlgNjYZv/6O2w9fsh39nb4dGMTYwwbhTY1SZKydCkl3W6XJE1dRpGoxX/zH/4H/F//7/83sJbE5FghSE3DEdOgwduEpzYBi5rp5LxggMXAEGdyAZZlEFmmFfRax6etaIMGDV4pCCFotVpcv3Gd27ffZXNzExUohIBWHBOEIWmasr+/T5qmWGtpnzwE4DheZ5qkaK1ptVq0Wi2EEORZTjJN6bZWGByPWdvc5rOv7/HF7mPSQGJfATPvmbACrAKh0EpxMJnyYO+A8XBKrGJuXb/Btc0tOlGLfJYwOh4wPD7m/kffAmD147+k1+vR7Xax1qK1RkqJtZYsy7h//z737t2j3+vRjmJ6cZtf+/4P+Fu/8zfAGIQxWN0Ifw0avG14Bh9Al0hdVH8CL9lQW88aZ+FCtalnr3omPoTFi18OscLl0tM3eNG4ADvKSy/x6XGxeqRZyng8ZmfnMX/0X/w7/vRP/pT9vX0mkwkWp8W7fv06rVartAJsFebfvWCVMHR5yYMgQGtNmqakaYLJDSIXqCBiPE34yccfg4pI82cwHbyArnWuLxKEBKlIrOXh7h6SgNl4yuhkQDKeMh2O6Lc7mDynHcXsf6fyAzRaE4RBaUofDAYYY2i1WnS7XbrdLu988AFCSB7ev08gJL/z679Jv9MlFOpqG9SgQYPXAhc3AZPhMm1qhND4bKlWCQQKrTUYi7QQSkUgJGmeldPKE8mhC0ghS4/kRSoMqSp51dTtwVYAqpamt9jNWnA8+6fvK4CgylTsJOHiT22No5x4gvw4Z6K2GutTTAnLRZPISynL+5hnsHFbXDdoWFrvZnp/dWDOMUmel5q2Pt7mx4pxuW1fOjSnB5/Lx+vhMwUZY7BJxs4Xd1FSEmhLol2u2jCOmaQJUgiU0bRbMWvJIQDD/nWMreaHyWRCkiTcvn0bbWFldY3DowOmoeVwOiFLNYGM0Ofm/D0LirP3yPlTlLccVhgsmUtPbCAdHJKkI6ZhxnR0SK/Tp7fSJ4xCtLXEcUwUx+x98BFZFBGPR2zs72B/8CMe7u2SpAntTptWq8XJyQnj8Zi9vT0+/+WntPtdVtfWsLOU99ev8dGNW3z81eeFBvAV1pI2aNDgynFxHkCry0MJgxK2OFzSdR9JJhEohOOZehquOHGGSVkUO+W6Odn/UC4y8vTnMyQfz0koC/Ny+fmCdZ0PivHV8OF0tdA6Yc+ZV6+WS88LgcuOZm5/dXDWc7qIiLJ0rAhDKQSeOq648udicdyf3gjleY7AmYDXVldR1m3EOnELJdx0pJSi0+vSXemTGc1oMmZ1sgfASfcaWZYxm80YDAZkWeb825KE8XjEJBmRkXM8GZBpDUYgzdO4kPh5RC05nkOnCoOwBmk00mge7dwnsSkiclRXWZ4xmU1Js5TRZEySpQyzlPsffAMA+cf/loePd1jf3KDd7SCFZDwek2UZxhiGwyE//cu/4vHuLkIINlfX6YYx72xeRwpZbJmbbWKDBm8TGhqYBg0avDB4X98gCFzU6nDI3t4eR0dHc9p+IQRxHDv+0TylnzgT8N0sQOd5SZYNThOYpin9fh8pJWurayAEaZa6+9kLqPNfISglOTg8IM8zsNBqt9je3mZlZQWA1dVVJpMJo9Go9AO8feczTk5OmE6nALQ7bTY2NojjmLW1NZRSDIdD7t27R57nHB0eMp1OsdaQ5RmBCnid+qhBgwbPjgsLgJ4E2h8+MENQI2U+R+tXZe44TfZSz+rxuoR7eLJetxC9rsS9DV4n+PH2Oo81gTP/ttttojBEG0OWZYzHY4IgoN1u0+l0GI/HpGmKlJKN9BgBTFSbnVGKUoqVlRW63S5RFLG6uooQgsPDQwaDIRYYj8aMx2PAkWu/Ttota2E0HjEcDlFKcXR4VEb2WmvnfCCnv/s3ANi6+yXX2m2iKCKOY44Oj3j48CHGGI6Pjzk5OcFaZzI/PDyg1WoB8M1vfIs4jN2Yej2m3gYNGlwRnloALP2R6mne5rJDzBc9lwlkQTgsr5HLI4FfVfjF2NqKuqZBg+cFv+l4nQVAYw1Gm1IrBZCmKXEcs7GxQRiGBIELZsgyRwzdG+wAsBeucuPGDbq9HoeHh/T7fUd6nOdEUUS73UJKt8m8f/8+o9EYayzmteozV//pdMadO3dIUqfFVErR7/cJw5CDgwOSJEEKyW6rzXBrG2kMG5/+nCzLEELQ7fXodDpFv7Rd0dbNWVIqgiCg1+1y7do14rj1TP7HDRo0eD3RmIAbNGjwYiEgDEO2t7dZX1+n1WoRhiGDwYAkSTDGEMcxWZY58uLxLgC7wQp7e3tMJxPW1tbIcxeIoZQqKamMseRa8/XXX7+2mzJjDJnOePDwAYPBAK01Dx8+RGtdEmRHUUTccsTPD775HQCu//IXBEFAGIasrqywsbFBEAT0+33W19fpdLsEQcDR0REPHj5kPHEa0k63jeV1EpIbNGhwFXgqAbA08db4+U6bfM9wWIcqbmPJdZeuyzNd/WyYD0S58tLnIzjKaIHncKsGDZ4TXEhUkf2jyD+rhKATx5gsZ3B8wsnRETrLGQ+HdFptJqMRcRixsbaO1Yat9BiAyeo7hEGALjKFDIdDhsOhIz22FiEkKlAEYcDO7mMMFlOwFbzyKOkSXIS4kgEPdh6BhHa3TavTwqARSoCE3OQcn5ygtWbn298DYOvnH5NnGUmSlsLxdDotzemz2YzByQmz2Yz1rU1uvncbpGCl2yN4bZxvGjRocFW4MA1MHZ5kVFmLRqJE8bdS1S6y2OXnWjuKmBoEAiWrTCCLv1+mDgDSWngJqZ7qvo5uB32VZhQfcUihyajxC9rstTKVN3i7YYUug+FjFdAKJO9tbGDHE2yS0g4i9CwhDEOy6YxWq8VkMHSExkKwPt0HYI8ucRASK+cr6ImOO50OcRxzMhkT9Tts37rJJEuYmRyNE6he9UB4XzeDJTGGBI01Gb/6+gvWVvvEcYywgu5auwziWOv10Vpz79Z75ErROzoguPc16qNvYtMcKSU3btwgSRIePXpEr9fDKEGaZ+yPB1z7xvvYXwreXb/Gl/IOxya/IHlVgwYN3gQ8uwn4jGCPZb9VJ7n/PbP27yUmcj9FA3O1pZ9zNGjwGkHMf7RGY4yufa6CWrrdLpsbG6z0+7TimDiKMMe7tHSCQfBQx4RBUJqNvV/xdDrl+PiYPM/p9fs8evSIncePa3zNr/a7U9bORdShggAVBCRZyt7BHgjI8ozReETcikGAVApjtPPJXllh5933ALj1xS/J85w8z4njmJWVFabTqTOn53mZXeXBwwfcvXOHlZUVOp028hXunwYNGjwfND6ArznKoIDXws7V4G2H1sZF+wuBxQkyYehyA3c6HbIsI45jVPH9RnIEwKi1hoxaJEnCaDQqCaA9wbwQgrBIIffpp5+W/m2vG3yktw+s++ruV+wfHJDnOUEQYIwhiiLCMEAqRavVwlrL3fc/AuDWF5+xurpKf6XPbDbjwYMHXL9+nffff99FTnc65cb75OTEZQh55x2aCaRBg7cPl4oC9tG6T4OSPoaL+2bPa9kaWXUZKioag30JZvAGDS6DIFCsr6/zrW99C2tcyrIgCFhbW2M4HCKEYGVlpYxm3UhdBpCj1ibXrl0jSRK01kwmEzqdDu12m3feeYft7W2EEKRpyuHR4Wsd0ODz+RptODo55ss7d8q25XnOaDSi2+0yOBmQpo4WZ/e7Li3c9S8+Q2lNHDual36/z/HxMQcHBxhjWFlZQSlFkiRkWcbh4SG3bt1ifXXjVVaSNmjQ4DngUlLVs2StmLv2EpNzKQA+1V3ffPiFrnS0b9DgFYYSqsxMIYSg3WqVOX09kfPBwQGTyYThcMhGEQByGK0zGAyI45jJZFLmuU3TlNFoxGw2o9/vAfDv//zfv7a0Jn6+85p9YeHLu1+Wwtt7771HHMdsb2+zsblBnudkWcbw3fcYd3sEWUr/05/T63ZZXV0ttIUha2trAMyKKGuvSV1dXWWlv0K71W6UgA0avGV4Dmq1q8iUvhD08FJmplehDqdxKh/96xgYvNiIi3SvXfj8SjySsxpin6qJrz6evVXaGoIgKDV82hj3byEAdrtd2q02QRAwHA7Zyo4B2A9X2dvbI0kSlFJEUcRoNEJKWZY1Hk+YTqecnAzw26Knb9N5x4uDsYadvcfs7++jtebBgwe0Wi0ePHhQkkPPZjM2Njd59K3vArDy078gy/PSV3Jra4tOp0MYhhXpvpS0Ws6kfnh4yPra+gttV4MGDV4+Lp4LWFqEcAcYhLQoJdz3UiOVKQ6LkAZLjhCUvizljrYkhBal+bKeCcSVZ4pDY8nL48XAFjlUNaCrf8vPxdJSM726TCCAle5AVp+tvPI1w0gX2bh4vPoChjznWI4yxTIgLWWeafWSgn9Ow5x5aMHSw74qVX8qLGvrJa62htWVVay1HB4fkeocjUUqxWQ8xuYaco01hm67xXrqUsA9Fk67l+U5q+vrZNaQGs0kmTEYj5gkCdduv8Pdxw85mgxJ8gxT2A3O725L+X5f+Hhx2sVMWGZa88X9u4xmU/eeW4uSijzP6fedr9/9+/e58+77AGz9/K/Z291lb2+PVqvF/v4+jx8/Js9zlFIYawiDEGmhF7f54MYtfv83fos4joF5hoUGDRq8ubgwDYyQFqmcmVEbjQSsAiUsWhiM1UgMCIvOtUtuLoJS4NNaF+mGCgGwzKJhK7MHFoFBFnQqxuZY6wQ/y4tMVWRwAh/OXC2KCX9hUjw9SRaCzKm582oXjDod4Jzg90oLFl7ykUvquXyxmWNZ9OrOUvArMjxcfUUvieVpxizu2bzaz+SycJu/098VuEBblZAEKmB7a5vf/e3f5ovPv2A4GKCNcZkqkozJZMZoMGTTDFEYMhkyiVfZaksmsxmHgxNW11aZzWZ0el2klI4XMJnxq3tfoZVA527OeLLLSiEAvqLPyUrJDM3n9+7y67/1m2hj6LQ7DE8GJNMZeeqCZowxPPrWd7DA+u4ONwQ8Fi5Cen19nSiKEFISzMac3B+RJTN0mqG0ZXIy4v3r79ButZimyTOzMzRo0OD1wNObgM9b3J51/qiX/dIX0YuKGItULc+v4l4WKs2/L72PLoPLUds0ZDivIJ7hYVgLa2trZHnGn/zpn/Lll1+SJgl5ntNutYjiuAhgEGwU5t/j1ia9ft/5Dg4GTKcTdnZ2uHbtGsYYtNb0+j1GoxE/+9nP3EvxJgySog3GWHZ2d3jw4AHHJ8dMZ1Nacczq2ipKqTJ1ntraZv/WbXfpn/xbRqMR/X6f6XRKEAR0Oh2M1qyvOdPw3t4eRms2NzcZDge0Wm1MQbLdCIENGrz5uLAAWJ8QqmCOq6uIpzLx4R6NCaLBIuq5cF+n4TFf77OPtwFSCvr9vqMvMYZer8vKygoUPnx5ljGdTlhZWWFt4gigj9ubgGMSiOO4jHz94osvynJvXL9BHEccHR2RZukzBay9StDGuKjdWcLdu3fRuS5zJ0dRzLe+9S1arVaZS3mviAa+8atPy2CPtbU1Wq0WQaAYDodYrAu8yTWHR0dMp1M2N7ccH+BTsjw0aNDg9cMlfABlSQUThmGZyP1ZjXB1XzptNGKJz6A3IzdoYKwpj9dJZKr8Rc8/3nQEYcjq6iq3b98uhF5Bt9tle3sLrV16szzLActG6jgAH+gWWmvH+ycFN27cQGtdXLdNGIbcu3+P4XBYZhV6UyiRBJCmKVpr7n59FyEE+3v7NWx4jwAAdl9JREFUtFot1lZXGY/HdLtdhBDs7e1xv8gLvP3ZJ4hCmxeGIdPplNlsxu3bt11+4F4PqSTWGMLQeQL1+ysvsaUNGjR40Xjlpao3ZSffoEED5wPY63X56quvyLUmDAOEkEghuXHjJpubm6hAMR6P2SwoYMYrN5nNZiil0FqTZVlBXozLcKE1nXaHh48ekWWZ8yV8Y4Tpau4bDAY8fPiQXr/H4dERg4HjARwOh8xmM4bDIfe2tkniFvF0wvrDe2RZVtLujIYjgjDk+vXrIASrq6ukWYa1ls3NTW7demeOhqZBgwZvNp5ZABQL/wIlW8JSse2JE8tlJx6fBcOCtUXAQO3zc8LVCKUFZcgLnmzfNrPjS4Vt/BbrcNksLFIIxsMhRhuyLC0Inl0Ks9lshkwnrJgJACftDVpxzHAwwGpDmmbs7+/T6/UYDodMJu68r776as5/7U0a3xaYJTMe7jwkSRImkwm7+/scHB5ycHjE0fEJJycDklzzsMgK8u6Xn5ccizdu3CCKI0yumU2nbGxsMJ1OWVtbRSrlgkX6q/Q7XTd3Fpla3iAOowYNGizgmQXAAElsJRGKyEoCDYFxR33SqNPBnLc793ksLzN5Syyh1YRWE5i8+mw14opnLp8NxbfnWaFfgvlvnsLmTdGUvFoQVO9BYCBCEiOJkKi3WRyUwvm0pSntuMVKq01bhfS7XWZZSmJy+lvrvBfMABgHXTa2r7Heb7O50iEOROlHCG4sdzodAP7kT/+EPM9LM/DrDh9JnktHPpNozV9/8nNOpmNEHNK5dp1gdR3R6tLurdFf2aTbXuP+Nxwf4I3PPuH4+Jh+v48Qgl63x/HePiozDA6OCKQiarf5nf/y3+MbH37E33jnm1wjpqVChJJlLI1ohL8GDd5IXIkGUIrTrG6LWo+6Kfe8+eSyu3ZforAWwcJhr1a4qaemuzI0k+sbiYLwpjjE3PEWi3/cuHadNE1dJo84xuQaVZA59/t9VtfXSNKU/mQXgJPWOkoKTo4OybOULE2YTMalaTPLMrrdLgeHh4zH43lWmjfAdaRUwAkwWIbjEY92dgijiH/0H/5jNq9dp9XpsraxCUgGJ0N2v/NDADYefI0YDnjw4AFCCB4/fsz25hZWG3Se0263GY1GnJwcM51OCXNLN4oxOne0PFQcnA0aNHjz8Mr7ADZo0ODNwdr6Ou12mzAMAWi328xms1IbnWUZrXab/thFAA86W+zs7DAYDNjb20MIwerqGkopfvCDH2CtZXV1lXv3nL/bm47ZLOHLL79iOpvxb//w33J0dIRSisPDQ8Cyvr5Osr7F8cYW0hjee/g1SZH+zWdg8RpSn0rv//0f/8d8eecOFsvGxgZBEFzAVadBgwavO55KAPRmQymff4CGXxgWNYOVGdM+1WRVN4POfTYW6yNNF+67eM3rgkWT7+tU90VUWWPebk3aawNrscaU2X+GwyGttovq1dqUzALr6+tMJhOOjo6w1nCdMQCD9gaz2YzZbFamNtM6Zzgc8sknn2CtJQxDHj56+Fa4MwgBj/d2GI/H7O/vE0URJycntFttkiRld3eX6WxWpoXb/vTnDAYDtNZsbGyglOL999/nG9/4BltbW4zHY7TWTCYTWnGLTruDkqpR+zVo8BbgqQRAL0BIcTV+cE+6z+Jn/7c7zDNvVutBEU7ws4UguCTDgz/vNROk3oTAjzJd4HMedw2uFm7MWaRw+WevbV+j0+mglIv2FUI4k3CrxQcffMCtd26xMj0AYNS7xtbWFj/84Q/p9XpsbGxw/fp1vvnNb5bC48HBAffufc3bMCSMMQxOBtz96ivG4zGHh4dMp1N2dh4xnU7pdrooKXnw4bcAuH33C9ZWV2m1WuUxm804Pj7m4cOHLjWcMYRhiNaaD97/AHBze4MGDd5sNG95gwYNni+EKMyO1qUbm07QWhOEAUop1tfXsdYyHo/5/PPPefTJXxKZDCMkduMWWmvu37/Pzs4ON2/dot1q88UXX9DpdPjud79LEATs7x+QvyHBH2dBComxlizP+NXnv8JoZ9b1mtH19XXanTZaa/Y//BY6COgcH9F6eN9lS+n1yrLW19fpdDpkWVaahZM04Z133iGKIt4KabpBg7ccF88FXEwIdZ4oi3VZc4XF1A4tDEYYLLrwIjYgiiTqwiCsRmKRGCzGafEoHOetwFonl0oLVRxH8cfCvOSdpE0ZYCLwFMFWiHoBp+B1YfXcuu7fwoXfFkTXdv78y6BiUfB1qizW59dskXvB1H57uVimjb3AVe5YGlK4/Hp76tfnlwHE+vpd+gbLF0rnuF8+9bkmPp2h0p4Z0f6iRoTjbZ5vyxOfvRAocGZgIdhc32A6mpAnKZ0gQgVBGbkbBIEz6R7cA2AQr/F4f4+9vQOEkGysb/Hnf/bnDPIcESge7+/RWekzmE0YzCZowdV2hj2LwOc8nqurRZX52geCwDRPebCzw/7uLrEKWeuvkGhDrjWzNCFJErZu3WD3/W9w84tfcuurL/j83fdJc83KygpSCkajMShJ1GrR6nRoddokwzE6z+mFLY5Gw6q1jSzYoMEbiQsLgJ76xFqLlLLw4dHkWLQ05CpHo8ltjrYabQsTqdDFYUAaMBopNIExYDXCakxtScyNwlqXZQRtsMYJgxaDJl9aNyNk+YsVYOYWKKd5WIQFt2AU0LI6zd2/uG/hE1i76tITf8aCuVhWpZ0tDejqR1ETTF6Ryfip/K2E4bLij6FagOzT3vcisBTj8LIShDqjOEsuqvGqnzEiXWAJWB7kkNsXt0hbMy/4PFEAtJZACISUxCqg3+4gtUEVe4GtzU2Oj49ZWVlx+YDbbVYf+AjgVZLEaQuzNMPqgJX+BpPpCe9940PavS5pJPhs9wG7yYj8qgVAqnlgoVFwxrO4Siw2J6/19f54wOHuHr/zw99ACEG/0+Xk5ISZ1YgoYqYFO9/9ETe/+CXv3PmMX/zBf4Wou8IondFpt7ChpN3vMc1Tjo+OWFlbob+2yihP2Ija7FvXwowr7tIGDRq8Mng+JuBy4/wkbY/bSS+eLor/Fj+ffbPiX2+2EO7zhSaumvbPVcRfV9/9XwGVr1hyXPSaK6rCK4NlffGkti32wyvRH2c1RMydcpUL6CvV/EsiDEICFZAmKWmaIgQcHh4ihCAMQ9I0JQgCbgUJAPn2u4RRxNbWFpubW7Q7HVqtNv1+nzzPCcIQC/zVT3/qNpzPTSv38nq9vONCFbQxfPLpJzx48IDHjx9jrSWOY4IgYDgcsru7y/2PXCDI9a8+J7aWw8NDgiAE4dLL5Tp3+YLTFGMtxlpWVla5cf36aW3z6zbYGjRo8ES8kj6A5wUrPE0Qw1UFP5SG2XOCQ17nIItXE87V4HXt19e13k+D+jtQb7d3H2nFLbrdLlEcE8cx62vrKKWIoqgMRDDGsDJyGsBBe8MJeoVpWErJLJkhpWQymfDBBx+wvb3N11/fRec5Ui3Xxr6pODo+Ynd3lziOOTw8ZDKZsLGxwQcffIBSijsqYtRfJcgzNj7/FBBonZMkiaN6AY6OjoiiiMPDQ4aDAcZoNjc233K2ygYN3g68cgLgmXQldj5rxkUX1iulP7Hz5c391GTXeC54nfv1ZWR5eZk4axPksuZIfvjDH3Lt+jWM0SSzGePJuDwnTVOm0ynJaMC6HgGgt2+ztbXFcDgsI4mxsLq6ShRFfPzXf82dO3fY399HSoXO3+wgkDoslsFkyKNHj3j48CEnJycATKdTl0pPKd69fZsHRVaQdz77BUky4+joiDRNS5eera0tptMpg+GQMIqwFlZXVlHqwt5BDRo0eE3xygmA5xrMioXlMoLc26N/eYPxGj5EW/v/2wyBe2077Q6/+7u/y0p/ha2tbW7depfV1VWCIGBlZYU0TWm322yZMRJLoiKSqIvWRVpIQRkscnJ8QhzHXLt+nc8//4I0TdE6f6vIiwUummx3f5fV1VXa7TZBEVCT5zmyoNbZKbKCvPP5J4RhyNraGkEQOEolKRkMBmxubhIU+YCn0wlRFDkuwAYNGrzReAUFQJiPgr1oxF09anYxgvZZ6nCZ8pbU+43G29beRSxr//Pth+d/l6tuk0DnOQL48KMPCcOQLMuYzWbkWcbGxgbtdps8z1FKsZU7Tdaou8V0NuPg4JBc5wwGA7a3r9Hv91lZXeHo6AitNb/61a/ItUYI+VZRl1gsAsHX975mZ2cHIQRZljEcDhkMBnR7PQaDAY8++g5WCDYOdokO9jk4OCgEaUOn0+H27dtIKel0Orz33ntsbGxy8+ZNRwXToEGDNxoXFgCNTbHFgcjLw1q9NGsG2FPmu7NMseVv1mCplU2GsWl51FGSMRuDsRpjc4zJMSYr/s0xNmeORKReH2vOMC3aIlo1d4fIa5+riOLT2TU0LnJXY9FFPdzxNIvnq591RJ9zvE0wvIh+sJSj8NRx9RHA5pzjYijHrzXIVkTQbrF1/Rrj0YhkPKEbxkQyIIoiptOpC0ZIElbGzv9v1LuGFBEqaNFb3SDo9DicJiRS8Zu/9dv0ej06nQ4HBweYgpHgbdqAWGAqNbuzI3721SekQc6N966jYkFvpVPmWxZr6xzc/giAm5/9nDyzYAOyzNLtrAIho1HC3v4x4yxBKUlooB9EKMBIi1EXDKhr0KDBa4ULC4DW5k6wsfUFzvP4LfH9saedws/yESq/N36R8YJU7u5r80LAOn2NsQWPoNXuKAQw9/dpAXBZ/U6jqIco6iJqn5fV2xb0IaI4fL8Ux9PglZ9wPbfjqeM0V+ObjWcXli4KK5YfwBX3uRvDp46neLbWWrTR5NYRPxtricOQlW6PzSI1mbWWIAgIw5D15AiAQXsLnRtyAxrJTBsORyNsEPCzn/0Mow2ff/45X965U+UAfouyV1gBOoSEnE/vfsbB8IDD0RFBrEiyBJ3nZFmGlJKjH/0mAB/c/YK1tQ3SVBNHbVZXN5AiYHNjm3a3xzTPWFtdI0SyEnURiGJ8vfKzUYMGDZ4CTzFjvoDJ4GnpT8QZn5+2Dg0avGp4Ggqdq7rPU5Uj0NqgteHjj39W5gW2Rd5tH41qraXT6bA63QfgKFrj+PiY6WTC4OSEPNe0Wi3CMKTb7fC3/vbfIkkSHu/vOg2+dzZ8a2DJ0xRrDcPhkDt3viRLM4bDAUkyI88dB+Xx8TF33/8mANc++wUmTcugmslkQrfbpdVqEQQBj3d2yPOcXs8J50KAkPIt69cGDd4ePHUuYJ8J5DK/LZ7n/xVlFo8L3veK6VaeV7kv815XfY/T9W4WhQYXg1SS8XjMH/3RHzGeTBiPx0wmE2bJjPFozHA4ZDQaMd1/RCefAHDSWmc8dlHCQkrW1tbodXsoqej1eoRhyOeff44xTitvjXnLBBWBVAqpFGma8MtffsqXX33J1tb23Ds/nU75en2bpNUhmk7YuPdlKUj7/gXH0aiCgMFgwGA4JI5jhCzIYN4i38oGDd4mPJUAWPqmLfinzfvFnT0ZL/q3SSnLcs8TWJ4nHchc2c9ZMHtRvn1X3V9VvU0j/zW4GIoNXrvd4h/8/X/AzZs3ARBSEEcxYEnTlE6nU/r/TdobHI4mSCn54Y9+xMrKCpPJmCRNODo+4uuvv+anf/lT/uqnf1VQM7mQiLdNUDHWunnWCvb2950QPZ3S7/cJw5AkScjznCTPefzt7wNw7dOPCcOQvb090jQtzedRHKGkZDab0et2+eCDD1jrrxXa1berXxs0eFvw9jjNNGjQ4MWjoCNJkpQf//jHDE5OCIKAKIyIoggpFZPJBCEE163TSA2729y8eZMwDPnZxx9zdHTEysoqW5tbvP/++/T7K8StmMFw8JIb9+ogTRMODg4YDE7Y3z8gSRI++ugjVldXATj4gfMDfOezX5CmLqAuDEOCICAIAqSUpFlGlmX0ej2++c1vsbKyUlhnmt1egwZvIp4z2+e8dnBOWzh32LksrMs1Y4tmx+dLtcGCZvNUPUTt8wuFN52/4Ns+Beb6TdTcyM6q+wtWNFxMA2sX/r0YGp1JBWcNMNy/e5fvvfsBLakI44i1jXVGkzFRFKG1pjt4BMBJd5uj4xMskizXdLp9jM4ZDQcEaUR7NebzO1+wt7dfPpW3ub+tgFwbvrp7l299+BGxiBhPJwyGA4ajETdu3GC3+yMA1u99SS9LeOcHP6TdarHzaAdhDRLL7s59VKaJw5C9w/2SqFtIBfpti+5v0ODNx4U1gLqgWlg0Jxpjyu/nfrMWrU35uydxBRffm2LIFZhQMtUZqdXk0v12pv+a0JTULP4zOVdOubGEusYd/p5FFLSYj4h+EbCA0a9Phgnj/7MaYS2B5dShXqIwa2r/nXeWe8ZLSVjOvEoV2Ra8i8PbCYs0FpnnrBExfrAHmUa2Ir58dJ9Pf/lLTk5OEEKwkboI4B3bZTDJmGlJb2WdPM+JAgn5jIO9R2SRZGQzjmYjcvvqvwPPDQKsBC0hF5b7jx+RYkitgUAxyVIyDF89uMdXWnNy812EtWx/9jF3vviU8eAQlSfc6PfYiEK2uysYaxhkCd/7nd/g2s2bxDKENH/ZLW3QoMFzwJWtTBfN3TunTxGi0gIKnhg4ckrz9pwiIOf0fae0fyyvw9usgrggzgpgfT267rJSqmuZeNv9p6xLBRfKkI3VNTZW14jDkPFkwmA4JApDOp0OWTJjbeYEwN1gxfEeak2aZRhjCYOAwckx796+xcraKv/mD/8QqZSjKRGv0zi6QtQabKxlOpvyeHeX9Y11JrMp0yIlnJCSLM/Z+a7LCnL9lz9jOBzw5Z0vGJ4cc7i/z8nhIde2trhx4yYffeMjfvhrv8bt2++hpESIJjNwgwZvIl6oasJaW7Nd2lMBJPXP9Sjh88676vKeZBW0nBZqn4yLReRWms9LFl+v21PV70n1eQ3szRfEm9imq4YfQ1cSsV5IDlII4jim1W6xurpGHEdkWYbFCYjvtASBzcmFYtJac/yBWhOGIVEUMRqNUEoxGo34yU9+wieffILRBvkWcf+dB1toQr/++muSJMVaS7fbxVpLnucMh0O+/PBbANz81af0ez3W19aIW62KVPvwkJ2dHfb39zk4PERK6cik3/ZNTIMGbygu7ANYN4V6c67WGm0ExorS1OuPXBuMEafMqECVOYQqKtYvMPXz/G9Q+AkaU2oJ565xFNVlXXXNX6VuJp2/BozVpV5nPirXLlUnWOvqfjltkIvUMxegxTmlbbzsvFsQY1+VX+JVCEqy3GPYwsz6cgUvy5Mpit5mWOs18VfVRwKlVFlckiQEQcA0yx0HnVBIKenvfQLAcWsDpCrnESklSjnqF6UUq9ub/Bf/9q9c/l+jG0G+Bq0Nd7++y2BwwrWVa3z/hz/gzudfcHJywsHBAYff/B55GNE+OWZ15yHJex/SDWOEEHS6XcIwINCKKIr44IMPSLPECYFZIwQ2aPAm4hKZQOa1AXUqk6Xf1wSqOQ1c+Tenf3uidu7s8rDLrzmnQUvr8OSF7/ILztNoDJ8Gjp7v1VkQhRBzR4PXBFc5hgpNXhRGSCEJg5DV1VVWVlZYWVmh3W6jlOJa7iJ6j1ubtFotkiRhNBohpaTb7ZKmKVJKBoMBX3zxBWmaYs+hmnr7IDBGMxqPGI1c8Maf/vGf8PDhQ4bDIWtrawzTlN1vOC3gjc8+pd1qE8Ux1joqns2NTbQ2PH78mE8/+QQpJD5LS4MGDd48vJL2k7qWZlFj83RxwHWNxlmfL1fiWXe4XGlPd9Xpa5/nQrh4n4sIyGdfc9nSng9ejVqcxll99zzq94LuIwTWWra3tul2O+R5RhSFbG1tIQvftDzPywwg+db79Ho9VldX6XQ6DIdDJpMJW1tbCCE4PjnhF598wkXI5q8Or+JYmYeUEgTkec7Dhw8RQjCdTpnNZoxGI9I0ZTAY8PA7jg9w6xcfl9pYTwp9dHxEkiTEccxPf/pTVldXCYIAo/Wr2uwGDRo8Ay5uAtYCowVagzECa2Wh8bMFIenZGsH6Z/AaN+PMTQtUIUbUzLzCFCbNIlpTFPOQwH32GrxzqarqEcI1E2SRs/fUZ3zdLpH4nsJEXatE3SR9/txZ1E8Ac/c8Q/spatcsQthLT9TnmXldH5zVD+fdyJRnzFP/PG1m5KfHoqBg0Syv+6uyuJ9RD3GVdTtH233FtG8CUMDmxiaqFTPVOV8/uM/t+AO21zcYyQFa61IA3AtWGI1GDEYjgihEJ5bD42Ou3biBCBQPHj1kOBy8II3y5cf+snfJa8Cv3FxdTVkuIhjIjGFnf5dE5+Q6Jw4jsjRjPBgigB/3Vvkd4MbXXxILy+HJMWvdPiJQqCCg2+0QhRFHJ4cc7x0QaItCkL8S70aDBg2uEhcWAPPcUUFpXQh/Boy2GKPJTT5HE1MehYA3Jxzi/fysm7QWo4SFxRQCjhYabd1ngyWv6Ss1lPPwuZYgUVvwRU0AtIXQ53OIinlfwcuuggZLXpqlnVB8kascnUxxO3G2eDRn/pZn1c8+VSjk2byLnu7mUqVR71dbEwY1XoB9sbBzAo+ndHlFIc6nlrk6+CeyrA5XaQKGXtTmnZs3CTotUCGtfo9eqwOAUorB/i6d6REAH+9P+fqXf4GRglTnhGHI+vo60zzl2vVrfPov/wV5rrECnntsqjhvA3Q2rLWlwFenALpqIbD+mKzPhgJ8tfOAYTqlHcZEcUQ2nSGMJZSKvZVVTlZWWR2c0P75T9m9/RH9fp9Wt8Ng7Ezujx894uZ775J8+E3+9N/9EalQ5LahgmnQ4E3DU5qAn/fEW7vFIr+DqC2Pz4374Rkm6cV6X7iOl7jnmVwqz8OEd/VFvsZcMC8GL5Iv5wWMoUxnBEFAkiTMkhmj0ZDBYECe51hriY8fIICxjDHtFT76xjdYW1tjdXUVYwyHh4ccHh5yfHLC559/fuX1ex5w6e/atNvt5xJ5fuqx1cZHmqTs7e2jtWY4HKGCgF7XRfqurq2x+8NfA+D9u3e4/d5tl1FFwGA4dBlB0pSHDx7y7q1bbG1to80rvGFq0KDBU+PSAuBZ9BBnaZHOom2xLLne2QzPmSxPl7cYGDJfL1uamU8d/r8l15/V3jO/rwWhXAbedPwsC8OrRmvyrG061a/PWkaDJ+Kq+6tenhSCtZU1rl+/jpBOAzYcjtjZ2WH38S7T6ZTN9BiAk/Y26+vrCKDdbrOxscGtW7fodDpMp1OOjo5cGrPXYLMgCtqbKIrK717UeJylMx4+eoCUAiklG+vrrKz0aRX1+eK9DwBY+csf83jnMQf7B9y/f7+g3ImZTCYcHh4ymUx47/Z7ZTq45n1q0ODNwoVNwFmWzZl5S8oXa9BWz9HDeBhj5871E4g2uvTtW6R60abKyrCY6UJrU07+i1QvyyOEbZG9w546z9iCMkXM12ER8wuj9xms7lvW4Wk0NLZunnw6vHKCjrWcn1XjyTA1X8inLelZ6/C24Uq1UzVTZ9yK+fXf+HUXTJBphFLkec5gMOD4+JhrN65z3QwBGHS3abfbzJKE69ev8/DxDsYYWq0WYRgyGo0Yjyelz9urCt/+weDl5Cq21vL5F5/zB7/3N9laWyeWAQGCVhiRR4bH3/4ORkpW9nZJ7nzOvgyJpMJimY5nrK2tMdM5j3d3+e53vsPP7vyKB4f7zg+oQYMGbwwupQE8j6blwmWc+nB5XPi+L0guepni1ysl/F0x3tyWvdkQwnH/KaXodrv8t/7xf8iNGzdotVvO5BvHdDodOp0Os9mM9slDAMYrNzk+Pub46Ij9/X3SNKXb7dJut9Fac3x8TJonL7l152MZ5ZH/+0VRISmpODo+4vHjHaSQGK0RQmAKn0S5usbOrdsAfPPhfW7fvs2169dYW1tHCEnuN+zGkUl/9NFHAA0dTIMGbxguzgNY/2zP1lud5tSbu7L298WEyafRcJ0SVOfu+iTBYln2A2/YpEpZJ6pf5s45VVd7znG6zqdM1Ev+e1Y8q8nvSbU7v6XLyqudd6pfnxZn1eLqxMpzn+wroaE6+2mUfz2HDYS1Fm002mjGozH/r3/5Lzk6OmIynpBrp0VSSiGFQGdZaQL+aiq4fu0avV6PMAy5desW7733Ht/+zndYWV3hF5/8gln6NALgxd7BNwXGGLIs4xeffsJ0NqXb7zGdTQnCACGE8/Er6GBufvYJUkquX79B3HJBI/1+jzzPyLOMlf4Kv/97v0+n3Z6zeLzZPdigwduBC2/pZjojLyb1PM8xWpNrTWY1uvC3M1hyDFoXGUN0IRII66JxvdkXjTZZGSmqTWFasLhzvBAwZx6+OKqJymJkdW1JI+PLO6NQa3OWEpYIS1bLHlIXnx0Vzlm1PCOyc4G2Zc6kLDVWVIs1clGQfnpchdk4J1+e3eScCNLz7lh/NnWKn6eD4cw+v+IlSwteSmTzxXFGxHgtQvt5wKdo0177ZNx7PZvNyPKMMAxpxyFrIqFlEgyC1q0PaYeCa+sr3N/bZzgw7Dx+zPq1Le4/fsQf/fTHTOvv36WgeXqHgtcLpiCh+uknf8l7H97it3/zt0hFwnAyIZaKrY0NHn33e/Cf/X+58dXnjNIJQdpianKiTpuT/TEm17z/4QcIA9dWN3jv5js8vnsXkhlQbITlucQFDRo0eMVxYQFQFwKe8/mrHabwthKOCsURJ7jz/ALjLB+mJhxU9AoFMYz7bM/WcF1UYFmm+fMLtF0UAM9YuCs9VgFRfW9qf8/f99xaLReMztJ6FppEI8wrGyFrMKWA+qyoa83KeJpnbvOLWZns6yAALq2fvWpZ+BSEECAEcRQX2YGsI3huOwqY69ev0frqJwCMW2sYBLs7jzg4OOR4PGWUZagwIMXw//gX/09Gk0lBXGO5/AAx525O3iRIIQDDNJnwn/6b/5Sv7n3F7/3e73Hj+g2UcGn25K//JrNuj9Z4xPrdL0nW1rh5+132Hj9mZ/cx/X6f92+/x3Q6RUeKb3zwEftff13d5BWdlxo0aHBxvJKZQBo0aPCmwJYZKforfeIoKlPBtVtt4uNHABy31jHGEEURW9tb3Lp1i+vXr2OB/+z/9695+PAhugwOaiSP82CswViLkoqT4xN+/ouf89Of/pSTwQl5niOlZDydcu+jbwLw/p3P0Nbw3R/8gHa7TRRFRFHEL37xCwaDAZ1ul83NzTfa37hBg7cRFxYA6xHAi/5qxroIYB8FXEb7+kjhWuSwWVJGHYvE0Wdh8byzzJr1LCX142VNZssodC57fb0dbwYs1r7c53LVuOg4flPh2yyERAjhTL9Zxng8JkkSptMpjx8/ZqPw/3ukW+zs7HD//n1msxlhFLG+vs4vf/lLfv6Ln5NrN7coqWg8z56MUAVY6wipZ8mMn/zlT/jLv/gLpJSsra0hhODBt78LwO0vPmN4MuDnf/VXZFnGe++9R7fbRQjBZDrhYH/f8Tbqhg+wQYM3CZcWABezffiFzguA9QWvfo0XDl2E2fkC4EUEpDkB9DyBconA+jKpU67i/i+7Dc8Dxrx5bXoTn9NFsBhAtfN4h9FoxPbWNgjBvXv3SNOUfr/PyvQAAHXrm7TbbbrdLuPxmNl0ymQy4au7X5bzhd9ENhrAJ8AKsoJk21qLUoo8y/nZL37O450dDg4O0Foz+K3fAWD9/te83++yfe0af+sP/oBvfPObdDodF7EdxTx4+BApBIFSL7lhDRo0uEo0JuAGDRo8RwhuvfMOH334EcPhEKUU/X4fay3HB/t0J04APIrXiKKI7e1t5zNY8OgdHh818t4zIgxCcp3TabdZ39ggSRLW1tbovP8BR+/eRljL+l//FYPBgP29PR49fMh0OmVjY4PV1VV+8IMfsLm5SafTedlNadCgwRXiwkEgxrogEFt8niNT8MTLi6bh2m+muA7ACjsXjFHPm2tFldu3/psp7nZamVIPLKE458mmimWax4ucd/b3tWAWd0L1k7DPGtb6HLGczMHHH9uFbxa/feq7vnL98eykFq9em14ePB+eNYZslpDNZiAUSkhW+n02tzbZSA6Q1pDJkKy3Sc8YRsMR65sb7A5GZFqTmrx8s3yA2eWDOd6y51IPpsIyy1KCKEJbmEymSC3Y29snCEIefecHrN+/x8Zf/xW7v/sHmDxnZ2eHJE2RgWLz2jY2Cuj3+si6vqDhgGnQ4LXHhQXApODvshKSLMca6yJ/a9k+vFnYm3rTGlFITi3DBxZdi8zNtCknk1xUv2lcZpDizNrnhcVWaCiSlVssxmbFZ87VHpQUM7aeCaSY2WpUNFVmEeapX+Z88HKgEjznKDZe6YnSspQyRTg6FlPj5TO61kfPqJWpp4t7dbqnyhrzNHgbzb1nIQgDsGDynFvrW3x4/R3GoxGHh0eMRyNyrelN9gA4aW8wSmfkWrNzsMv6xgayHfH+tW/w7nsf8PEvf06Gz7ZjETZHvEKj5lWDBfLa+2msIbCCo9GALAcRh4yGKcPhAb13v8H3gVuf/YpfJRlHxwN6nS5xu0VvdZXv/8av0V9fY+/wgD/e2oajfaCQwd8UF+QGDd5SXNwEvBj2f9EE9cU5njy5TsWyWF5JAlMvt7y+wtJF9jkms38y7CtQh2eBXZJdvoaFZ3HVJrnXrbcanA8hBAKB1ppev89qb4WVXp9ABUghWF9fJ45j5O5XAKSb7/LhRx+xuroKwmmndK754P33+eijD5FCLrxaS8brRY63AQtttrjnkeU5YRHdO5slaK0Jw5Djb32PLIppDU9Ye/yIVqtV5jDu9rqsbmywdv063/7Od9jY2Hi5bWvQoMGV4uKZQBaza1zQwf3sc87OBPJ02T/O/OWFaGZsUYfL3OdFBQksC365qqwiV4HXd232bgmN5q8Oay1plmKxXNu+xve//31WVlerwI+VFVZXV9lIDgHn/7e/v0+32yWOY7TWxEHIl7/8nPHRgHYYoQBlHR/66zteXh6MMcRxTBCGfPvb30YpRafTIer12PnGdwBY/9lflWb7PMtJ04zJaMRgb48sTYnC6CW3okGDBleJC5uA8yIRuLV2jurFR+BKKcvvpJTkSYIWdilJrrW25PTy5Xk4Q3FV9jIIIQjDcM70LCVVxoHaecYYlmasuGrYp8us8KKoXOpCiim8ORUKIcVLcU8UVDlTLdQ43l4vGGNfzPh6jaCUIm7FLu+slNy5cwedZkgpabfbtFot2u02veEuAAexoyV59OgRaZqilGQ2mtDpB7x38xZ/lKYoKsVWQ0ZyOeR5jlKKzc1N2q0Wx8fHJEniAm7imIcffMTtT/6aD/703xBf7/D5r/8dvtwbsbu7y4///M9pdzvsHewTReHLbkqDBg2uEJfWAJ6l7RBC0Ol02NraKgXCxevnvzj7HhdBPem6EKJcbPwBYIxuNDMF6v0gEEhcPzletRcP//xctoiXUoUrQjO+FmGsy/rRarX4J/+jf0J/ZYUsTZlOp6RpyvEnP2ftf/2/oDU7AWA2KzYC1hJFEaura4RSomcJWytr9KK20wDS0BZcFn6O1Fq7/MtSMpvNUEoxmUyQkxE/2vs5APHhEe//+b/mD/7F/5FeIMmylL39fWazGScnA0aj8ctsSoMGDa4YVzqfaq2ZzWbOjPAsgpetRc3WP58BKQRKKYIgIIoier0e7XabIAhrjkOLBwuf4cKLed0M9RpGwwkESrr+Uhfh9lrsrtesvQ1eDPzQEAi00bRb7VLjdP/+fcIwhP1d/tE/+99x87O/cNdMMv7O/+F/T/7oIVtbW44ixhiEhW7cZnN9HWNyJLY4GjwNoihibW2VtbU1siwr58r3//oPaedjiBTCghwmtI8e88Evf8zJyYCHDx9w584dTk6O2d7aetnNaNCgwRXiwiZgb0712r3FzBoAWZaVqYaCIEBbXeXhnYu0PV22/1fpHFEYeaSlpHwwVpDVpn9rBbZMxCqQUhEECiEEURQ5R+YsJR8eoWs0NaXwIiyBkoRBiFKKNEtJkxQVOF3DdDbBGksQBGW9BaBs5Tkn8NorgUZizlBlnScM+7bPfYdweYCvEPX7lBQdT9C4Cju/Q5C1MvIrrd0VQ4BAUhJhvKVaYDfcz95ALRt7l0U95skHeBlrwFjSyZSP/+KveLe/iYpglqX81sc/pjWZIG+sueuPp0TjMd//yx/z1a3b9OI25AaBpNdv8fAgRQSQp5ocMG9JPt+rhFKKKIr4m3/zb9HqdTHqgLjTJghD+rORE9qvdd046YRYIWiNT2ALuq0Oo5MhrU6bb33jo7LMAIH0c+EVjKMGDRq8eFxKAAS3mNYFiHrmD1Fo4gDCMCTXlAKg9yFcBm+ytdaipEXOBSoU5wBGSUrdW/GPKXzYnLnXlZOmKVEUEQQB7XZMkqVkWYYQIGQV0iqlBOHyZoahIs8hCCTr6+uMRiOOj4/nzNnWWqTxdaqHFjq9h+W0OfNJwt+yRdi6kq4MZ93HPbszhHL/71zIdtXeZyNMeb4QxX94IfeVrekLgD07auKqBcDyPsaNG5Pl9NodtHVekkEQcqvTAingwQBmGWQGKwTs7zM+GXJ8cIhSiiTP0Gt9rMjJycnQZMXrL5pAkAvDb4hnsxn//J//c/r/vf8h1965STqZ0m63Sa/dRnz6Z7BZkTwLYzhZ3SKKIpfBKcuYDjWdmrVAWEMgFfrtfrsaNHitcWEB8EWh7js4l+9WgFLzfA5uARPkWpDnGcYYgiAgyzKyLCOMQlQQENiqnPqiZ60ly7LSPwacGfvw8LDUZPrz6lhcfN7UCfCsRfZNbW+Dq4NLQSYZjyd0eyGieJf237nNN7V2kRwPhoDz7Uu+8S3SNAUoLQiz6ZRHj3bI8uwlteL1h7WW2WxGHMfs7+8zGg65tXUTEcVEYcjDX/97XP/sJ6w9uoOVEmEMOzc/4uN3vo3NXOBOr9tjfWOdlbiKAi41f2+pdr1BgzcBlxYAjTGlEOUDLupawLlgA1mJED5KWABWCoQ9rS0ThTlVCB/EUYswBqQ0UCSXB4swIJVAz2wtGliSF3kwhRRokZe74HrdfARxEAQI4TjL6mZuIYQzYxcRz+eZS8WCVnK+TadNrYvaVCllGQldCagvMjjC9SdAEASu/7BVuxq8vhDVs32h97SWLMs5PDxgs9MjjiK01nz8nR/x7u/+Ae/++b8rT3/427/P5z/8TXSS0Gq1AAiikI8//pj/5P/znzjS+QZPDSEEaZpydHzE11/fY627RmAFNsuJ1jf56X/nf87qX/8h3dkRo9UN7n74Q6L9A2QYsr+/z62b79Dv93l496uyTIXE6MLFpzEBN2jwWuLCAqCUEqVUKQD6I45jjDHOVKB1RRcDWFRpAg7DsBSCMq1Rudvtey2cR65toelzviue7mWWzjAByMDVI8syBIJWu8fRYY7W+RwtjK+rihQ1ObQUsHw9fVn1YIi6QOsFXK11KSQu7R/kmevsmenksEghiaKI6Ww6d70UAqQqDSx1/8nFCOv6fS7t7yZACucv5yO5x+Mxuc7Ltjd4fbGo8b4I6uPrWTgO0yQhyzJW+iv0ul0mkwmtTodP/2f/S+7+xZ9xfTRg0Oly/N0fsNXt8vXXX7Ozs8P169fptFrs7+9jc9OMwWdA/dkls4Q//bM/5bsffRuhQoIgYDqdQhiy8/2/yViPQUC322X01V3a7Ta3bt1ib3eX45Nj9LSKAm4Mvw0avP64sAAYx3EZOeY1ZXmez2nQvBAILofv1GRLp4lMa8JCAIR5gSvXLYLAac3SNC1/i9sxNrIgXD7gJHHX9Xpt8iwny3Th5yfmzLlR2CIIg7Ke/j5aazf54SLk6ovMokazLuAuX4wswli0rvwc69yGdeGyvqB6DZvWGimkc55fwGJE9Xl+W0+zUJelFeUmSeLaaF8cR2GD54NKmXz5qPy6lvqycFH5AXnBCnDt+nVW+32stRwfH7O3v0/8ze8xjiNiDDpJysCtdrvNysoKK2urrK2vMUmmaNMw/z0tFueLk5Nj8jzn9s13mA5GtPshx0fHdLtt1tfXmUwnrKys8Df+xt/g008/5d69e2QFgffg+Hix9BfWjgYNGlw9LiwAaqOJZEgYukukDBGihcWZX4WPzyiUBxJoIUsBcI6TT4IWXiiqTyKWXBtU4ExISkkQONLYlS47hw8xWNIkodUOmc1maJOgAkEUBeTamS6lUk5Y0xqd61IAjKJoTjh0gmtemT1L5pnTAqAXcLNsSd5cQCJQVuEzQ2S1oJezSLSdFhVynbsAlcKjfm5avQT1iuAcl5zz5mofqY1llsyqqM7CRH0mzqvTG7M2vJ2ajmWC32W+y/MMJdx79uDePR4VZsjt7W22NjaJWzF5mhAKS6fb5fHubpkGcpYkjB/v8HhvF2MtUiiwjRB4FTgZnHBweMB/+x//Y3765z/hxs0bREJxeLgPFqIwZDKekOeO0cFow/raOtYabt+6vVDaYvRPgwYNXidcWAAcj48J1AoqCJyLDwUNS2AhtAhjkFojskLwsRayDCkEaWHiVcpJh0oa4qA0bmJ0TdMkMxdjKkCG2pEWK8lv/PZv8Md/fsj3vv9dfvpXP2U8mRC3YqaTMSoWhD2DtDmjwRQpQ3cEMYPhkBXpfNuklLRaLaIoQuuMIDQkSUKapuTJlDiOQTg6mygIsECW5QRKoEJBaALsNC3NwUEQFOZTUCgC4xY8Yw1hUDlMz4RESFHS5HhYq8nyDCkVSkmCsDK9SSswZRoVgTljjnXaSsrzzvLHMYLlZQhBuxMzTSakaerMvvrJmr8IiS1ChK2pIm0NZ9znJcI/I+Apo4ItiOVR7KENuOwCmGPRr7BgeZ7md1Hgm/vbuI1fpBShEHTjNtdWN+jGLTbW1jk8PKQXt1nr9VFKYcOQ8eiIySQhR5OYnM7qCvuDI9qrfcL1FRJhSa3GiCdscBpcCHmecPfrz/nxT/6E4fExnZZCZAaBRdkQFShm4wkbm5v8xo9+jdHRMb24hZ4m6N3jspxYBRxb9068Yq97gwYNLoiLawB1xnQ2KU01ABYXdCGU05oZNDKohJagyDZh0UUwR8H3h5wLMJjzuTMK7/Md4pjrx+MRP/nzn6CU4LPPfsUPfvh97ty5w2AwQOucVGva3dilFDOQzHTBR+Z8DweDAUEQ0Gq1yrRIUgqCUIGIkEpgMTVtV1AktIcwDOa0gZEOyXOBlM7UPB/QwSniah8NKQp/ukWTrqNh8dHNlcbNcfAVZWPLz4uYZ/k445zip2WyoVKS9997nyBQfPb5Z4xH4yfSg/inWLF+2IXfXh0stuXp/NkKFexi0JKt+uIyJYlXWPh7VggAYwomT0uv2+Xo4BCdZoRhSKAUx0dHALQ7MWme0+12OD4+ZprMWFtbY5LMEIHi1u13iVstpqMmCviqIKVgZ/cRx0eHoDVaZ3TiFlK4jXaW55g85/7duzx69Ij33r1Nr9Plwedfktb2BdZahFLYwl2kQYMGrx8uLAAaYxiPx86vrvADBMjyDGudn5tSas7cWU/LVg+gkFIQyGoZnPcBNAXLXIW1tbXKj8nChx++T5YlfP21pd1uczwcOkVk0AJCDrMTtC5yYAa29CfM87wMXmm1ojIThvdvnM1mrlMKKhlwAqT35/OpqlRhYoZ5X6m631/5WYCSCqnUHIn2q4I816RpygcffIudnR3GTbqnBs8IC7TiFr/+o1+n1Wqxvb3N4OSEMAxJ05TxeMzm5iZBINE6Z3d313HSpSlffvklq8X7fuPGDeJWjB0PX3aT3hC4OfbG9RtkWYZJM44Oj1BrG6ytrjIrgnb8c4rjmLt37zIeDLm5vkWvW3EFKqmwJis2u6/atq9BgwYXwSU0gLrklFpZWXGaLGNL37nF/L8+UMRTs9SFPGMsCDlH2VK/ztPA1IUqay0iEoRhyMcf/5xOp0On0ynzWoIAq+h0YBRPyVKLtQatXURwkiSMRiNWV1ex1jKdTgki67QShSBojCm5yLyv4GIUpW9nnSLGtcmUlC5BEJTnCgRWVlHNURSV/UmRwcSXXafYOcU9eEY0p6fXWYYLabsE7OzscHh4wMnJyYWucXK4KTWdr1NE4JxG0L5edX9WLGpDn9dGxFrL+++9zz/9p/+Uz37+KevtHnu7u2xubpYbSGMMySxjc3OT2WzG0dERcRwThiErq6s82N+FKEBK5Uz4ZxCWN7gMBK1WzFaR0s1aNw+Nx2Mm4zEra6sEQUCapgyHTuj2G+Ysywhr4+W8DDMNGjR4PXApHkAf7Xt0dMTGxgZSSdAuYMP7xS0SOXvUhTmDxZiK/25uIRIaijRo9fRxVlhC5Shh9vb26Pf7pUYvDILSINluK/r9FUbDKWk6xe96tdaMRqNS4EMYbKbn6re6uspgMGA2m82RQC8unF7oWraA+uvKbClULHteOPSCntYWz5Nz0XRxywTDpdlELkjfIYDpZMJ0WrX1QkLga5oU2Jn2K9P625Im7mpM4ReFZTgYsPd4zwl+H67wzjvvEAQBa2tr9Pt9AOI44uTkiNu3bzOZTBiPx1hrGQ4HtNstEqNdRLFUzrejwTPjRz/8NX70ox+xvbqOsoLB4RE3btyg1+lwNDhBCMF0OqXf7zOZTNA1pocwDMtyWlGMzC1Wm0YQbNDgNcWFc6uXwajWMhwOOTg4KPjzZPW7dRpBf9Sv8wKiE/icMJgkSSkYPukwRRRunju+v6OjI5IkKe/hfdKUUnS7XeI4Rsla6jgcxcnJyQmTyaSkdkmShPF4zGw2c3QzcVxo/+oCzrywU5/uzj5ruXjk+RSVcmbhp4V9wnFeHS5+j+X/nVeHF4sntf5J18Clar7kNk96DmfX7Fnqffm7vVgI9o8O+Ff/6l+xfe0aaZpwdHSEtZaVlRXCMKTb7aKNodPpcHh4WFoLPA2REIIojhBSkunGB/CqIIRAScnO48fcu3+PLMvY2dlhNps5vtXZrJyb/TPyDAjeRQZgfX2jJORv0KDB64mL+wDitF7WWKxQ7B8eY4RiZXOFzGuxjCXPa/57Na1Dkll07rV5gtxojDaooJZPWIARqdMC4kzFpuAAswJMOiUIg9JHRUqJFBmTcUJunUbNaMgyDRha7QgVhIzHo9K0m2UZg8GALI8JwkoDVCeETvIp0gpynaNk5e9ngTSv4je1mTcBa6+txJYE2HXBueoWp/2U9rT8/STNjGU+yvbs8+0zReN6DV8ZiIytNvrCclZyhosuB3OBME+1iJwt6JTayaXFmvIaW/5dL3MZavxGC2cbLl9/89RC2uXOL92zLkjf8swQ/p4uAlgC71y/wZe//AxTCHuDwaDUJg0HJ2ysrxAECpNZdGogh0jF6FbEx599yv7RCSqIXTo467dzpnE7e0oMxyMOjo4Q2nDr+g2mgxG91T7HgxOiKCIMQ7IsI01TZrOZS6upcxIMUe1d0XmOxGX0ax5FgwavJy4sAOYootglFc90jozaHA7HzJAY4c2slQ8dWIyu4kN1npdEx9bYUlgCKiEPyMQILaosIaaUNOavkbKIJBaQaUNmKkFsOp1iLbTbLTY3t0iSmfP5K3zzxuMxk8kQGcyXp7Vma2uTf/Af/G3++A//mN0Hj8t8w75+s1xgl0x5Z5mEfZ3qBNTVeQs6oQuZXsHUko4Ya862wDzjzGyExchKqJ2jh3nGsuvm16c37uUuFPdU4XYpqXahi77kHb0kv/xVyTFzUdBPRlEHcVleO1/3S0BQRJm/IAjXs0pJNtbW+eF3vkcsFVIIVldXSx9Y757QbXd5Z/sWyWyGmcHudJcgiIhEi70046effcEo025826DkyXyWEfO24/Mv73A8GrCxukZvbZVr166x+3CHdhgRhCGdToc0TRFCcO3aNfI856uH92mHghvvvleWMxoXwWJNEEiDBq8tLu4DKATTIuBCaE2r3WYymTAYDMiLRUZrXRMA57NheNPtIrzZGJxQkIkJWpw2+ThawXTu7/KzBFMLnkiSpEgjF7C7u0tSZBqYDyrJ0WlSNM1pJKbTKXme87O//IQHDx4xGk1c0IcvG0FKEXCygLqQt4h6P8y16bILenldVYX65yuHqP37Vs/zZzX8KbVoYuHfq6jKMrxo36yaRXBrc4tut8vu493ShyxJkjLbRxAE6DRnZ+cxo+Gw0AxKNjY2QAhGoxG/+tWvsNZZAiofxsbk+NQoOE6PT074/d/9PR4/eEQvbqGU4tr162TTGUmSIKUsrSxaa+I4ZjqbsbPzuCzKGO0oYBo0aPDa4sIC4M2bN9nYWOev//pjwjCk1+uRpilZmpSL0mJAwuJn/7fPrLEI97v3v3OaQic02oLvrnJCNkaXpjdjDHpBo1IXRH2wSF3YBIMtuPG80CilJEkS/vzf/yVSgRQhudalgGsBK5cTCZ+nAVwMyCg/i0povKiTfmlatk8+98I4i2HX4sieX8E1t6xS44D+ykFrw+Pdx5ycnBCtrpPnOWEYopSi0+nQ7/eRUnJycES/16PTbtNqtbh79y6PHz9mZXWVvb09hsMhFntupHuDyyEIFLPZjEcPHzI6GRCsCGLlloEsy5hOp2V/n5ycMJlOaXc6xHHE+OBkrixPy/X2bgwbNHi9cWEBcG9vjyRJyly7o9GI8XiMEZCZiqplMfK3/nkxvdoiXJYGgaWgRsECzt7prq/7oDgCZSElKrRIZUoNnzGm1Dp4h/LTuXw1SE8KLSvaFiEwxpJnpiaQVv5fWudL5aEnmYDr5/lo6SgOMFZgtCYMw1PnLU2zhSO49oLgeZrHi8HOmSMXn9mrxFk4BwvG5R152TVpsAABrK+uEUVRGWS1sbHhhL6Tk3JM6TwvA0Amkwndbte5b+DcNLxPrs9N3eAZUdB4/dm//zO++81vgTFMJxPWrl0nTVOUUvR6vXL+nE6n9LpdxnlKGEYE7VZZVKCCgq6ref8aNHhdcYlUcGOOj4+JoojJZDIX/LBsF3huyiiYo4upQwO2mFRkEYDhfPpMyZkHEMcdgiAgCEIIcjLrzBeewHnxPotEzcs2rsLluMMaiUCdWnSc7i9fuuN9Gi6+VtxiY2OF+w/vI818dpTzUC/x2TUj1S7+1DOj2eE3uDws0O/36Xa7pKMJvV6PJElK1wyllCNezzUG994FQUC73SaOYzKtGQ6HzJKZeyca4e/KIKUiDAoqLAztThuA4+Nj0smUjz76iL29PSYTl/Xp5OQEIWFvb5d3onZZTq6Xu/Q0aNDg9cGFBcAsywiLHb2nBXA5dTXIun/Ok2drr0VcBmVl6djf6/aQUnLEoSvZmFIekYVmYDZLyeyE1EzRC5OSj7b15t+zs3DUHerc33YpQ86TJrwlbS/9ok7/po0hy3LisEitd0oAu9AdLvzrZVELAD59p6cUCs+77AwRevk1T8lAUY//vRzOrvlT9/qyC5cFtTzhovMueR5E1wvOCqc+Biqg31/h5OTECYDdDlEUY4yjF4nCkMl0SjJLsJnLJORdNuI4hiwt39uLboqW1ue54ip3RS+ozsK5zmxvb/Fbv/Xb/PTHP8FozWw2Y62/QiSdebgVx0wmE6QQxHFMmk65+c47zO49KouKoxiRTl5s/c/FsufxpHo1O9sGbzcuEQVsyZMZCIGRAqskmTXkJgNrSk47bfIiWk9gjChdtOo+bl7489xfXitgjAGfUUMpAgv5LCEWChlIcpGX3IFZlqCN4whEGQKlsNaUZktfrr9PlmXzfodWEMhWYaZKHSmzb6vVGJYEolBQ1iylHtFYe0ZQR01YsRjn+4dgMhkyndrS3FsXAI2o070smNaXWsScidzY/NQvT0ZFjTJXbQvLxHQLpPLyQqAqmuDiqsWcY39+Rq/Kc828l1t4LKCFxcgzRGuhl5dpFZwR/KOfyjq5vCxXh4Qz2yuWfy+tOxZhcO19slB5QSwtqvoiMIIIATpHCsGtW7c42t9HoGm1ArLZDJ1MmRx36Xd7bG5vkGQJw+GI0fiILJ/S6XQAw6AgJTZ6MX+2AIIzOl2f2UdXC1GMicsKEPqcZ/H8hSgLoCRf3vmSvbv36eSCbhwSqAAZBpjJjGw8JdeadDx16eBkwGGaYYGD48OyrLjdxo6PEArO5IR6Ybj8tvJi1zZo8Gbj4jyApf+ecRQbQrh/pQVMLQOGKYQZsLbyn7PWljQQXhPnI4PDMCxz7uZ5XpIlB0FArhTCWpCgFGT5zKWPwyCEJQgEIlBo4cwS/j6+TB/ZW0/b5uD8+6yRGA3WCAoLMAaDWRKhu0iEXP/FSbrnLD6lrOPqvSjULZTmjppi0iyesMwMjQWrr2ROE7Vj2W++GpeBFdX6NxcgBGCXa/osFrFsUbcX0zafLq8SrE8pEYU59V2ZM8QuflcPRFmukFz+nah8WxfLKjrHZ8lerIddkLj9X2cJgODeG8QVLnLndLlCEOCCCkyuuXv3LkrAxkafg8M9+q0O+Szl8YN7ZP1VtDXIlqMfabVcerj19RWSLGM0GiKFxAgzTz/kWnxGDV6QSdJ63+DL4umi/p8KZzxyoSSj0YgvPv0l1zqr2CTDRAHGGtpxiziMOD58zOHefskJaLVhOpnywfsfluX47EvG2isdXk+FM6lo7JUH8Ddo8CbhEjQwxb92/m8lFUI6k67fqadpSp5ppFSnJod6IIiHF/aAudzA3jcoCAKEgrgdOUJY5qlVRGDLcryg5zWK3mTty6vf1wug89qFJSt9WfmLddWV4rIULFc8GZ9lWFkucDyhrHOuObO8K6agEQiUkeVf8897uSbUByItxVONiTMaY3Ekj5fR2NXk4FM+reC0M1eoATzd3tNlr66scv36defzB9z7+phACUySYdOcRE442T/knXffZXhwAEKwtrbGZDJhd3eXqBWXgVlKKgIV1KL6z1vRZaGZe954Gh6f8+z0gkskZXoyxNnp2bIsI2r32djYoB92CALFKEnYimOOHz9if3eP0WhEt9st585ep8PBwSFbaxtlOb1OFyUktsgJ/8riFa5agwYvGxfXAJ7hO1fX5Hh6FxfN66NqK188T9K8SBeTZVlptvUZPqAS2Ky1CESpGYT5IBJTaL18RK8vz6eOW8zC4eEDQ+r1WfQ5WoyEXfQxLMtbou6pC5z1fy8UuOHLFpe45rw1ZiHzxkV8wywGW9O+ncresbQIgbKyktnqOwB79p2XL9sCawWmtjb6sfXkui8/T1p1ppAgpKi0b9aesYbOl1t/7KL2/0Xvu/nfFsur/6lgSYaY03sRW7vvfGbm+hiWtnpOV+EPeNonzwKz6rMQrK9vEBXao8F4xNpalygIUAhavRY3trb5/JNfkiQJk6kz++7v79PpdGi323R6Pb77ve/yJx//lPFk7HIB1+94LvP5ixQAL9OfXk287Bpx5ph8KlgBou4KUj2zOIr5nd/+Hfr9PgcPHmOMZWQz1q9tc//+fZRwGkKtNdvb26RZxq+++oKt7W1UTQMttcXmurD+LuuP5TnKwT+/856hL+u8HfjizvCMfl2qlz+rzFcLZ/VfgwZXhYv7ABb8eV4r5wen1nktx68p8/M6H0CDEBIpZZmFw5t587yiYKkLap62RQhRluvvZa0lDMOSXsILY7nNsco6k3FRjuf8O4uX8DyKkzmBsBBel6HSQoqlGRfq2T/qZZ8VAFNH5jNM2KrPnwxZzm++/ct4BpVUVXT0OaZoKRVCldJDlckFiGCpD6A1pkz5JwApqjrl1pSuBHUIqDE8zkMLyFV1Tb2uWuulQo04Z/GRKOSZno0SvyCYOUFzXjOoTy1Ei+Us++0iJuvCv+1UecsX2Ko+lV+qoAqcWN6vpVEbe0mTqWWx7aeFSqUU3/72t1lZXWW9v4LOMj788BYr3S53v7jDdODoo65du8Z4PEIGktXV1dJPN4oiDg8PmUwm7v2lCuCCJ22GnsYv72lxWXPuefWSnP0GXAzzz0HXBM1iPBS3j6IWP/rRDxmPxk7w3tsnFZqfffwxUa6JWm1WV1fpdrt0Oh2Gjx8TqYDx8YCHyUF5h43+CpFQaCvLUTQv2J3lQHK+ACiFKi+TQpb7R21MmTEKKChoau09UwA8a96c9y1+1XgmG+GvwYvAhQXAVqvigJobnEIhjCtmnuBZgA1QKjgV9OEFGi+E+Yg/X7bXBM4JadIJF2mWOr8UW2UHkEIilEDISlDy5Z+3C10u+FhCpTCF5usUfUxtoojjuHadxtgqcKR+Td3MfBFtngUiFWBlJfhWBNbnQVB3gq/X7yxORjibkgcx71R/FsfjKURVfaSoFp/MmlMCRFFraln50LqWOk/lZDWBeVFzvKwe5wntASFqaVo3izYJlT21rusyIHO8L6AnJz9VgrWlgCtY0FKbsxPGXSgFoLVzC2BdK5bZKh2ddSdX59XK8G4aVXmXEwANkC48waAWkRSokCBqEUcR29vbTIcj0Jp7X9/j8c4jPnj3tssCIhWdfswsTRFxwGg0Ynt7m5OTE7a3t9HWoHNdBIPNm9/P3Qydo929WlinYbuUad2b4pf5swZgIp5FeDW27sqiQPjNjECpahxOJ1N+9dlnvPsbv8fW1habm1t8vb/D/cePuNbqF8wKM6Io4sGDB6ytrXENy/b6BuwfleXkSYIwBiWjKlNSnevVfbG0rk7TvqQbwG2kiy4yUlQ+w6jamBdIWV+Dcpb7f0rOSuFYT8f4SvOdNmjwHHFhAXB1ow+4lzRJkjINUCxbCOkEDaMNcVqu/ggRFhG+QWEONW5xNwarDdkpcmaw1rP+uzzAwvuXSBCB00pFgShMpO6n1KRo4czNSgGiRaCU8/E7k5/PoE0lVM1ZKkProtsAISVxHCGE01ROpxP8ilQvWiqBUnWBLSt23qLgSqxNjHP1WDCrFnD70+qaOSFhQbFU7rmFRlBlQKkLIFmWzfeF71YpiDvtqkgv+AiBMTmm7COL1qY06afTxLVJLJgFBVVnWuZ4GVNOC4BCCDAGmVdagTyr8kbn0pCpvCq61qZFcnFXH3fvOVO914BYLwCqWhm+qgaDKttUbUoEQug5k9rcsygFPqd1FLJqu8WUlj8hRa286nJjrEtxWHuOyyRFryHzF+s8p0hjgxK27NdlZOxe6FNBUC7Wxlr0BfIEz2mQpSQKg6LJbrR2sTDcBaDX7pCqAK0N3W6P7Y0tDvf3mE2O2VxfYzadcHP7Ouv9FfZ2HjPLZmSZ4dq1awShIstTjMnJshSjNarQBjnhxtdVoE4J90Wf2qAUAEthYonRuqr9adTPtmL5WQLrNkZL81CfowMWppwT5q+pR5kv1vb0d1UdTfGTcJmRClcZF8xU9ZGsacuUMrSCmLW1DY6PhwRBwGAw5NY777KiIqbjCbODfcJkRpImyEBx8+ZN8iRlZX2tLGe1t0o7bGFNgC0EM2/RcW1ym6Hy/aznca9tRNxmzbdHzLnN1Dd72isX3IMmzbJiqhHld+62C2ZoGZW9WNekG7La/G3n5hVREy6XZTvx73Hd6uPfifq9q/Pn/66wEFxWO0VrPa9MrbuWnLHvEAvnLm4slxnEl5YjnIZ1UVN7Vl3nbzN/zaLV7SKazfNcturf1f+ul73svMXfnoRl973IuctwlpJisW6L516k7Retw1m4sAC4/dGKv1XBz+XghBtTfq5rqlptRRCqgqLFVVIFilgEhDgBTeeaXOcoqVCBqkyG1msMi10alQkSC5PJGO1NwFgyYci1xtRNxjhT6rLHZ0xCmp/gB2v9GhmHRC0n1KZZVqaqOhmcoPW0LGN+eQgRNvRdhJIuEjkIgrk3by4lHpbUC1gWcl2ZbkyeY/OKN6Vev7SmCTI1DYwkJWDGste8rv0xxpR9hxTIViW0h0GIVBIpJFrnrk5UGqggCAmDAJOkKCRBoNyE7vtESDRVHGuSpEgpUMqZ5/19jdEInDk8TdOa68DiomlLLaQFzBma1fLZS4lgXgNbr5+xbmNRll9oeLXR6ND5mCqpmCUzcp0jkeQ6J0lm5fmz6ax89vk0QRqLCpymW5STryZNRvhRonNdaqiVVKdeWK8dr2Nxc+TdJ7TWZaS71ppZzR1CG10GSlncu+GFWakqsvEUQ2Krd6u+QDvBsJSMiz4USKEwVmGKFI0CiGoL51pnnSyKePRwl529A7733e+xLiUH94YYK5lOJwynQ46HRyRJwiyfYmzGcBbR6gV847sfsHu4g4wtG90VtlprGKNRKiDzQSBeu+vnCGsQ1gveTiD22lYZyEJ8EqVFodakGqrv87TmM6wsdiE6SUmJlAFZVhNoahYMY2vPURTWibJfFdLX31abFAtY6YLmAiXn62dibGFhce4a2rlwKIkxM6Ry72AYRljrLCdoQyxr72RtsVhf7fHh1ntYEzAxsL22wdos5fqN68SRYDwacZxOGIxGrG2uc5RMuLG+wsrGGtnJcVnO8WRIW4bEQY8waCME1ZxCpYXXRmOsRolqqQmDYM7Pu27eT9KE3Lj3UdpqvCY6JzF58f5I2soJuaq4R7lAFmPElafJzLR8vkrUtMMipD7G62Mjqwn+otgM1q1YywIZ3Zj0m7/6HDNvQapDSFkK53UrjxDQbgW1+XrRn7mSDH3/qZrvu9uIyrl59Cwrlj/XtUEQxy3SGtdveW6hcKnmtzMEFXLq7hGL/vNee79okfKWQO8u5t2/6gJSfax49hBP7+bXN18OzKeArf+26BZWzo+1ceiDzpalaK0/f98+H3R6lrC4+H09vWVdqDv1bBZ+8+2sy1nP4i5wYQGwHXkfFUEQVAuYM9dV5tKSskFYlHI7VFH48KjiRQqVIpYBUaTKjg/8AlprjB8Qy1Cf0HMMqTGOnb5GGWGB1OaVj0r95SXDsInbUdmCX7A4L5Cl+VUI4fzajGF7Y4Ncz0ohLQiCcrdojcQYVV4ThWFZh0UOxNIHUAh3r0JB5dpaaGuMRRTaI2vt3OQqW1EpJdUHtCRDlQ7586hrIesvvQXy2oSnlJqbjOovRfkyW0tgKx1DvQ4aZ5Kswz/fRdQHeN0vsh65vahcPFv76Rb/MuVf7UVqxXH1DBZffiWLTYgmRSOVRBW5Uf0kCBD651mbHABsmpda6iAIyvOceJ/h1QdxHJVR8X7jUzbKOgoj7wvr4dwniv43lizPyLPcbZhqAqCf9MEtwnkhABoss9ydr3NdTuKube598W3SuhL6tKm0D1mWkevcccWpAKzjjVOB0/aEWQr/l/8NAL/9ve8wky6d2IPPPuPg63tEoWItFkgMR4cH7Hx1j9lsyixJwGq2rm3QVoojrZkOTsjSjNksoW0sP/rgAzfuLSRpUta1rlXWxk3+URjRabdRQUCapKR5Spqk7hnqHJ1VQoI2ek5ImwuaWXFCh8UWVH/V3FafnJM0qcqrC+pWYM6Ys4wpNsFeWPCaaGvJ8hwlBeWltlA7SaoAYStAKWQxLlTQoT6sBQrR6aCEJBTylOYKYKUd05WSo50d1tfX+OCdmxw93uHo0UOEdJvnXhSQWM3o8MAtbGsrTLOEva/vluVE7RadbhcZrRC32njfZv/ulM9H5+S5Zjablv1ljHGBezhNU6WgFLS7LTc2C6qv8v22ktAE5SQgRFCuD6eEslJoCeioStBbXPhFzcVGm8p1ScRhOU94NxO/PpV8tZwWbnwdFtets9x3Fs9bFIrqda1/P7e5XRCk6/P0WVmw5pQQNbctL4CHYUSvF8+1yZ/vnosljir3ojlhRBqkrN6ZOsIwnBOm622qP78kSQiCoKSMq5/n2zibzRBCEIYhcRzT6XRIkoTRaMRkMinXEH+OUqoUFv296hrrZW5Rnp2kziG8aGE55eZzAQHQC6K+rEWZZ9l1/pn58xfv+7S4sADYC6oHnuVZbRu9EBNZ801S0ml68tztAq00zk1H5CTW7QZ8Q+opos4KwFgcOP68SEWEwmmBbK1DLWDDyqlY57p80YUwqKBfK6z2MQzK/MZCCNIsxXFeGSwpvvFGV873Rlt07mZcHyHr21MnjxZCVj4zEoz3zxEUpq2iTVIRFDvWeY0YJHae09BlUAaBRJzxSKUwc8JSGSggQNV3l7n7PZZuAjBeC2kB44T/IAgIlcIaQ+59tWSlAbFmfrfpB279eft/dZEHuf5s47rA5s6ufa4L0zXNi6mizq2dfynmXrC5EkQxOTqhMcMig0pAL3efQsw9g7qmtqVChHUmWTMn+Fq08AIgxULl7hUGrblxXW2AgtKU59q0OKE4adEY184gqAuA82VR3DkveACdJqPaYAUWQlOVW+//+cnKB3i5++a5KTUmxhjEdFKe+3/+w39Bg9cAX/xk7s/feooi/tE//m/wX4vbiLBL1Oogi3fcD8RS6+k1ObVr1YLPd31zFgaB08YvbFiyPC8I+/NiYZdEUUgYRqWLDkAQKFQx/gWggurNPbX4F3Ovkoq4Va1vohWV7fBzuBMWRKGF8YLb/Dvs6+qEqvpccFbAUH02mhdA8lyfWvxhUVMoyvXFbx7rAsJZpsX6PKy1LjdUaZry5ZdflsLX9evXaxvaqh6Lm2BfvhCnNYD1371QFkVRee86/FipB3LW5YEsy8rD+6lmWUaapty5c4fj42O01qXwFscx7Xab69evE0XRQowCZXpKf7/6HOiFvjzPSdO0XE/SNGU8Hpf90el0yv6vB6bW4dlP/DPwAu4iTd2icLm4salbgC5qyn4SLi4AylqUWm1QzExeCkve1QqKZc+AtQGBdH5OJjcYYdAYEFlJ+RLHcdmJs1mlwVoUAKtdpjo1uL05USlZ8xESCBVWL4+qv2ReQ1P8Ve94Dbag4hBSEAYRQno/m4jS9yc4gxbaWNI0ccKeFIiw7o9T074JsF7Dg99JFdokKwhq6vd6PwRRQNnTtharajOMVcXOUs07StdQ513UWjM4GRDHMXEco3VemuutNaUpQ+BMqWHBy4isKHrqA9UIg5FVXg+/26lzNPoAHZ+AfnFXWNeoGCPASqIoQimXqsqXlcwKn7hCUCt39dZNoH4SnEzHxFHsNEVSEQhvRrAYrYnDFkErILGm1MbOmR8KAVBKiZCSiColocgNgZQY6cwRru8FudFEcascKy5jDYhYlvVVKigWFTfO3CJqCQJVmIRrgU7SjZssTQutnMZoJ8jrPK1piA1SOPOIDCTD0aRokyDLs9LUa6YJZpK4NiEYj8dkeTZnUvEafd+vfvFxC6JkNnXuBj+6/j7vPq60Qw3ebAy+/0N+5+//fTcekW7TX2iyPdy8GZZzxKIGpL7J8iY/Y0yp9fGbRr9oSyEIpCrLKt93IZjNZuUCvqjV0dqWrjj+Xl6oqTRl1QbIAgl2bv5avJ9vSxgG5SY0y3L8RmpRi7OoDayE0Pl5vVrsvfnytIbPbeJKlTBZVglL9TmrNA0rNx9Np5XGum51o3Ar8H3+O7/z22jtXK+m00nlWqLn56O61s/PF0EQIJVBysrtxGvtpJRMJpNSQPMCU124k1KSJAl7e3sMh0NGo1EpnHqzZ6vVYnNzk/X1dcIwpNfr0ev1+If/8B+WdfB94dc4T2vkhadFzbEfa0mSkPr5tRCml7nm+OuFEBXtXbGe+XO9kOoFci+c+nr4Z1BPiFH/bVHoHA6HpaJkOp3OCf2Lvv5nuRwsg7AXFCV/9r/6ZOn3MzQZfrG2pf+RW4Qtnisky7PSPGtkjpWp01ydY79eZqsvc4T6XZEFjC3NzItqdafOX15+nQZDyUpTaKTA+h2qFEhR8NoJiwoq529r6lFvBlukYTPWVj5LRb/U21EKpMKZm8sb1+optSkDcL32xp9jlFzaptxoMpuVkddnda2n5nGVsEhDaaKvi7Sy7qPitZqFhszWfJVEzTZrpEbLKprWm0z8JFGfIM8yjUyn02pXZAJ0LueFQmuZTiZOI6Vz0iQhSVJn0rROCzmdTJhMpkynE7QxWGMZj0ekwwnCazWxSCEJQrcoiDh0moY0nfOJi6Ko3Okt+uVlkxmT0agox5mbpJTkVhOvt8pnFoaR004UpMYl36V1Du1SilI7HIYh2hhUsTmK4pgwCLBF38xmU0ajcel6ofPK5O0ntTiKCOMIwqCk69E6r0h7i3cmUJVpyw0Ht2CGUYSt+eW4yTEhy2fl83aUTCFxFNGLBe1uTLfn6ENarVaZjUcKQaAUYaGx9MEnoZKIwGlUtra2AcuXX96h0+kyHmfle+O1FHXznizGUX3H780q9Y2GR33TWH+GWrv8xG5425KeKgwDtDAIBVEcubYEYbkQTWsmTW/a8v3n351ABbTb7bIOp97HmhastBQsmNz8RsFtvELCMPBDl8X9e6kNF4D3zZUunZ6rj8JmCZ1W7DbMgSo3y0KAisAaN5YEzj1CBQqjTdnPAKlSZd21kuVc6eaLyvpylhYeS9mOXGuX8cWaYmxacq3JsxwElZXBmJKHUEpBFEXkudcWmbk5ptSAG+HWIE77UqVpOufmUhcAdVDNr4vmUi+khmGIC2r0vmqn1yiP+oK8qIUMguq9q/pLFKwbtqZ9922lppWzxXvBKbh72FIJUT8nTVOCIKDT6RTzc0XjVjfhp2ml0UqShNksKftkXjFTE3hshrVVcKfvW++y4jVqfo73JtxWq1UKVLPZjE6nQ6/Xo9/vl/3ty8myjPF4TKvVYjqdnuIW9vPOonC5OEcsE+LyPC99KZfBB+P59tXPqwv3dYHQv9v1TGe+HF9GXXvr+zvLsnJzNJ1OS6HYj6coikqB12tC/bP5Z//sny2t/yIurAFc7/SWfp8oS1aMBa11qRq2FtJZFbFkdMUBZ2SGDipH+rpPTx31neOixBwEYSkDBVKibOGPmOdFKHAhe0pR+vPV4YmlPaT3mSl+LdOFFQ7mflpL08qsF8dxrQxdpW6zEITVPRcHRmkplGDq5Ko13ispJV7EmnvhBBillgqAQkqMcRO9VGouYGLuPCERhXlZCohVUC6s9aASkxe5nil29GGINAohFcaIsr1Zlpf3skpjw0r4LdX5xh0XUWGPRqNycp5ONYPjhPF4QpLMCqHAlrsrLxxkWV6+jG7RrJtnAlQgiOMOHRET2cqROMuyQssHs2lGmuXkWV5oQl0d00nGZDB1C421c8Kh0TmdsOV2kLMZrbCNsBKjM44OB+WQqvsQhWGIkII8c30Wxx3CKCQKQzd2/IRRBEBIESKl47/stHuAmFtUut0OUeGjK4QszGIxURwR9TrlYh0GqopwDAJUHKEKE0tWTFam2BFHYYixljzzwRey3ABJ6Uz+WptiAo+IJKgF528/KWtbEcKHYau22GrSdIKxhnBlBSEgHo6xUtHpuKATX1bdHDMcDivNknCblEA6k08Yhgjr0sclvu7Wcjw4KceoW0wqZ3Rdi0JtdxwHXr/fd9R80paLlH8uXotUFwzqKN9XIeaDa2q8lXWuyrplY1F4yPNsbiyW49oKjAnK+dUHbrl+dXOyLDavQRgSKBdkl45HTIym1W4xmSUuzSbFBjZJCQoBajJxpn0VOP9KFajCt9ZgdcUMkKJdoBbzG8Z5q4WtfF6ptH6ufW5BlMoFyHh/06AdOt9rDUKC1M4v2l1vmExdBHMch0gVuo1fsTZo7eesgE6nO7fhrWtZfB3q86vFWbVMbd2pC2ztdrvUKPrF22sf69aQSnMvS5PnMiwqOTy8cCALrWfdl61+XhjOu0z5hcFpNWWNUmv+PqbwmfeWM9+W+lp8ljnSGDNnAh4MToiikJWVFVQAQphSy+uFzCAI5vg+/Zj3fea/l1KWlHN1c7bf7Hmu0E6ng7WWbrcLQKfTKZ9hfaNe94f0z9Lfsx444jeCy8zuaZqW860/z5fbbreXKrG8IOuDVbzQ6YU5KWUZxDKbzci1RufevcFptf04878Nh0MGg0HZHrexgW63W/ZNmqZnMp8sw4U1gHv/24fl53pAQa4qYcl6qdY6f6FZXvmduQFU/CENVnlH0XlNkBXOOGtNZWZz/l1uAGpjih1tNdiDYvJwmh6DrPPGhTBP11DzPamd5xYlJwhkuubPJURtV2sdYZ1YMolLi6xFDNa9xryjutcelGZV6fIYe/NJqb6XAiWq9Ghu8ayZjYNFAbDQlFhLZiv1+9zCVJbtI13lqd+kEMz1lLCliaO+o5NSOt+y+t39eLAZk3To+BrTjKTYbXY7HTrdLkGdiw43Zoy16Nozqk9yTlgVbteV6yL4QFULa6GNyvOs8o1BFCZ7M/ei51mOzfJyIaGmRbYUgkDNj6ec8DAYaYnCiDAMamPZEhgwWe6i7oo6B0FAbjUDkvn+lN43B6zV5eTscmSHRFGMkiFSBriIzgxjTRE5KggCUWiinUbRzzt1DZRvszPHKKdhntNmFO0TAlNOrk5D4BfHuN0lCCInSBSCoZASJSAKDCpw6dnyPEMUmi5hNdbklUAiKy19HAalhdAJCK6fLQaEqe3CIY6jon+LtlJN0KUJzhRBGsWO3W/mRCCRgcAH28xNbQX9VH1hdwKSQkkfsOU2FraIunTcov4VseUuPQgLwaN45l5wK4U3C3VfTo9E56c0tV4LrNNKS1EXLgPXkGL8u7mjtEqo0L8ApcYHQCgJoY/WLiIshdPoSaXRRRBRlmW04ppwYrzZyz0LbyaUQmGtwBTaqMrqYUl1Wgm1tXfAGkfzVVTOBe0Vt/GaZYoaR2FYCYB5FQwVx3G5WfABcX4cefcEYwy2MBFL6ZglxpMxaZISBCHt9grtVptWu1U8Gzs3X4FnBjClBjOX1ebWrUOFgCskYRShlPfn1eX7UzfLOl9zU0SMC6yuPZs5gc+vf3rBbGeZJklBfu1Mk84ca8u+Ku+l3X1UoLC62pzWrTLOKqHKujrBykWPIwSGYrMgK0uAKPrIl+Cv94WL2ga7bv1XgSAIZOla4vtVCEGaZWW/e1/8eqAd1mm1wigs2UHSqbMmuSC0yvpXznd+DCxYrsrfagwlxphybXAWL1Uod7wvuMYHhdXL8/OBLzPP87LRsyQp3626wsorw6y3sgQKa2zJ11mnOvP1dMFPjj7OU9i5jbbGmspU7dcMz4mstWMGCIKAyXRClmb8j/+jf8JFcPEoYOLyIeW1HWlUG7fWWrJc4OlDZE2TZkyN30kHSF1FVWZpVnWwFGhryoAOP/Fq7TR8WJDIQrgpbOQBEHgeLVm93MIWfn7zvGiA0wzWTKlSzjvBV6ac+u7QkqtsaQYMWdsxuOvmg1fi2Dne2lzU6idKh+XyOqWK3boTwE7V20KeMS8A+gXeWOScNqLWpsI0GagAYUSl5QO09IK3Cyzw7VDK0opdpHOapOULbLQlsBXNgI8ULe+jI+xUk88yMAFWSqwMQAaVaUo4c7PONRiNVbUGhUHpJ6nNlFyPiu4SBDICcqc9rQlpEoMS9cEISjhlsChs6VEIJhBzk3/J3VjspvyzDWqvRmpTUp1gZE4qJUEUOLNxECIzjZ4aprNxoYKXdOMuUbtFO2rNZUmoBAKnCfI9aKzGSEilYTYdlxpiS7EIS4VUIGUtQtjKsrIiPW3iInPPIrGVA37doVpaUH5dEvXxKpimPRBeO1J1qbCGwGS+twjCAM+xmGEwQpTRwa5v3Vju5G5KBa/l9i+dKB3xXd1BJ26zlAuLLoSarBDulXIUOlrXdh/S95TTmJpC0zSvYYN2EINwBNNZnhWTqAu8kbWgJYJ6dJ0uTeYCAYGbs3IsRmcup4wKQGuE8Sam6t1CzDt1Z4rSXKoNWOsWy0Aq2u2KaD/XzodWWBc5rIybn1QrKLUCQkoSk89tMKp6O+2OLOgFBZVFxAagWu7vuNNCBdWCr3RYaim8MBoEiiyxJNOMLHfvm9doCQS9Vqd6FHUB0M6T1/sN0+J5i+ZEd417Fx3bhFtYM6PJjC43BXUNv0kyqG2q48Is6E2L3iRYeLEshRCVb1dqaprauTYVdEhSkqV5KTyAs6RoY5glCVpaF6yXp+hUVxtOiqhi79pQ01RlWc5wOCBNUpIsJTXOVcIYQxxF5WZhZWWFlfZK2dbtjc2SWioUFTODE5QKH0Dh/Lkt1UbPW0mMmGdtqMexdaIQJSphqvQzFlXwot/El/0o59fBqmCQUVz2vx8bQWHN8u5hFkf1FcWKtpTIFbuUbtM9ryX3gULQ8tpiFphBaoJ/4QPu5Zkwap0qCyCZJeX6hnCbQv+eKOXM2skCbYzbQFVMIM4VAcC508ymM0r6scJf0SmIqrkoTXOkqPxTjbUgHFftbJaRphlpls75FDqlwtJmLMWFBcCg5htwltLQS6Z5rlBaY3VNsKOeF7eQNorvsryKqrSyGKxakxW+Un7nvRgQUr9vaYK0ztfJQ5tKrWtq9A9WiML/7nSb5jVQtcW1EACXTSOLu8p6ef7hBsXOvjRVUmj2yvWwMgUFoUAqW5ZV+sThONzKHZeS1e7T2nPUvzUBsO4TInB+YsWfc5rVEOJWgBSyjJha1j5vQvX100qAlcRBmyzN0FlOMk7xiVJEsX0yRbtyrclr2tMoimtCd4pd4DYsqy4qPaurg1g4oVjYVDAnyFTyt0BXcg95zQtASVkF61gJuSTJqwCJbqdD2I3AKKbDlOFoAhbnOxdLRBSgcjk3IVf3zzBF2jm/E7fWYrUlCINKsCt2loEKQBjyfFbWVdbzBZtqYvMCme8GKaspr36NtE6w8Cc6LUVROAZbCNPeIRrcIteqzRh5klNqoJTTZkvtXC6UkkgrEMb5LZaLf+19UkVQUQUByiACg1XilOO2MI6gPTgj24eklAPc5qLUqggykWC1Qevc9blSTrA2qjQ1e18aj7pmdV5QsRjreDCVUKUGwWa2YGrx40ZURPa4DYn/K5QRKgycX6gQCF1JJ7Y21yorym2cta6BQgsXeZ7qpSynQkgi5TV7C1lxlCm5DZWUZdsxFp2lLsGJBhX4pInFXBhI0E5oqW+OanF0LoV1OfxESbhurTOj+RfPSDvnKzi/nthCEyXd4m0L/zQhUEFYli9qu/AoqDgPg0DRa3XJetqNZG85SoybK5f4zljrfImtccQzqTXlO1RPJqBzpz21OC5QbyaHusuDJC+CqZQKiMOQ9V4fX2AYVmTsUjirgvft/ODd94v53G2oKLRyWeGW4pUDZX+JeWE6ROB7wgti3ndbiACti+AI4eqghAQliEo6HMgLZgthIZ/m5HPWknKokNocKypf0YofsKKTEqIibXdCULUZ9Rota90zi8JqTBnlNNfWWJeMasmaZoxlmkzLDWz9NRC1OAC3EalSBeq8Eu61NuiCJivPNfkZrmj1tjv/wwme3sj7DAeFZarso8JSiajJJF7pYh0bhPOrnJWm/rqFBlzf5RSuUzqniiyvNPu2cNnK87xIrJFzlny2DBcWABdpIs67iZTSCYBpTXWKWZDKnaBhEUjjI8X8jwah3YE2CF04rEtZRkvWO1sLShOitZZMu5nWWovJhZ9lsLra4VjpNFl+8HizB0A+58uka1K5JasJgHPTVi1iFuz8rqMQdIIyGq1S8VTmFEpTjVIKoQygi0nQDVRbmC/qQSCqIEZ1/a7mntM8bLmr9y8eOHMS0UKgTNEMbTOkNKUJNq8J1rZm0veTDK7G5NgyWMRPDHErLoXbOIpBUIbgZ3nOOJ3VhNDKF9LaHFNkN6l2/oWAWtNolYEM4tR84ExqeM1LtQlwE1SlHdG1eJwgKEx8gBUGpC6fsdY5s3DGMB4TKMV0MgGEo6/JJMlQk02nTPK08o8SshBQnauAkM68IpUkiuLK/0NWfWaswfvoOB+vokFiXmNdd8mYM48A0ywptZCyprEQ1gX/+CLLDYYAqyZVEFTt/whLIpYH7iCo3AuKgA83KQqMTZ0GsBD+BF6bq4iDsIyMj6MIlEGGYCRkaVI4ec+c2VI6DjxBVDNuVZs0LXL0XP2qkRBYAaai56icwgMkjvfNCQzzG8FyITFVKj6LxdikLMM/A53nWCmQYY24uVgEBIJKx1H0Vc0dIzDVC+jNd0KIKuqeYj4zptDqSDe+fN/XX2Ah5gSsuY2p1EjlFt92u02/3y/mSU0ynBSckXnZfqUUyBCDwpvdZ0OXBcj/Xd1WnKpHOXrq1hE/DmquB+WcaitzXRgElcuHdPO8f6Tet1gAZNppiYQL5rPWmfLTLHMuJ4WJ1tR8MOdgvTbOWa4yKjqbeZ87QRi65yG0ROpKGJNWFBrvgG6nWwYbYCwmodSAj0bjUgPoN3dK5cyE4MQOSpLvmSnMkUKUvJzeZ26Oh9TakrYlRJZuQ249ccE0dfL40sRbRD4bKeayCJWRxIBN80ox49dmXJBRvsw6VXw2RQYeKZanbbTWzQVSVvNv3QRe+XJqpLbFJtpJUqUyxxqmk4WkDLVHWz5nO1+/NEur16F4n+rRub7vRO1fr4DydUuztAzOUVKWGW/qMoTXLPr2JklFHxcGIWEUluZk71OZ5XmpTbXFfXXh/5drXcoNthD+/JzlfRK9Am4xCv08XFwArD3IpSpe6hOUG6ihUuVDqXeitQJjCgHQWkIqMt6pTqudiZBEQpErN4izNCWMY6IwKv05oBAAvRLMWKSwhWrV0JKOWFQbjSQoqZuMsGS1zpZG4Stb1VVgDNjcC2kWXXN6rvMAnu6M6qOUhZYhA52Z2uK1kLbLOOFJ4yZpp/WpwtFLbVmNuNn7bgSBj3DyD8n7W/lnY+Z+qy+gec33bR5VCjSXsqzaIc1tCGq+VUZYMltlZfGTjiomqTiKiIuIr2Q2I0lSkjRhMJss3VS4wIvaol6rZxRGVQon64RZr+Gsa38c8e7p1vkIYN8PWe1p1ndjSkJtgzpXTxlUEZHz2mIwc/esJighDVJVfi3VWLHObOeF0Foebfflk19svzkC50s7rWnho7AiVnXuArbiBPPzpQAtdRUEVS9cAKp6FrLWr6rGbymEoBW3iOLIad1FVmSsCMrxrlRAiCCiiHQuItqyLKXb6xF22kxmU05OThiPxzU/IYG1tU6y1UKak5LatKysqAkgNs1Roohq9T6FrscRNig3Up542rfPC2PaVAuEENYJUeVTq/n+SMqgEuezrEvBwOqKYHtuDAHhKU65QkhSspjo3TtaBdBYpoUPpovmrfvW1saacBuTUqOVTmnHLqCov7JCdCtgPBwxm07RScpsOiu1Es63LEDLAF34brpIaacJ01rPzYF1VoRFmMKc78/zPG1CCrI0K1fc01trt8jlWIxkyfvtNNmBqPVDYYY31hT0LAsuC3iLQfkAa0TQQBw6VwUpkLKaR6QQ5LXxX0rfFBsAm5GR14Qg3LxUm/PzmiYIXJBNK3aBUaVgJ+atEXVXF0cIX9GF+fU2UAEmy7DZPKecH4Oz2YwojIjiaG5jroXbbPlrsqzi+FXWlppWW0Zpu41lVssRn8ySBQ5Uyj6es9oV71GdisW3oRS+rHUWA6+gqLFhzGXZKfvfYZbMqrml5q9ofHac4twsq1kl6z6C2pRk83Pli+JdrQuXtTFef57zMo6dExonk0l533q2M98fFBufOg2eq0L13OuZ0Mr74F3oiv5a3IQ9ARcWAM+KVjrrPEf9UPEFKqqIOGucAzq4jlJRC62cpGsSQ17sfkIV0ArCUtWaFAtiIH3alaKMUypghbagrcBo6VZi43cexTmGucy0dUdPaWy1ehvKge/8tlQpAGLmnUWX9gfO3FOPGKw/yHJ5FyDDahKXhQYwF9XuRFuNsjlW56XpR1qJRCJ1gLQVjUsZ5VxTXdcnEV8HS+EDWNTDDSyvpTA1P0Rbd6UkT2vM8uX/3AdrAQ1WQy50oVHNmInZnBljliTlrnuml/sz1V+k05h/YesO+XMLhPTaN8/DWGiFa/4VThCuvdi1l1liUKUQOl+f1FajaHHCq2POPwqNlKaoa7XxAJBK451egnomECsw+izt7nJYnH+PLz0IVCUwa1OlsKvV2Y2HzGk9WZh4JIiam4yPRAMIhSDwe3UhaBX0NcYaRKugDwrchtBrzmKp6IgiijiMMNY4bkEBVknSPHP0MzVaBoFAipj6wuvHdS5y8sImaaxxVCIFYmQZCet2/P4dFLjsJlXkfLW5mvdpq567BZGVz8lRSLnzUmFIljktUfjylZsAUW7QJG6z618hT8GT57kTJkv/2GretYVw74JwvF9tJdx4/yiBICqJ1S3SZHRb7tmsr6+jUsHh0SGT0RilLaPhkOFw6DItFGXnQUhCZZJP08SR+2t9Ks3iee5BHnVqEB8A5Oo9T8c133kKGRU+m1LOMRxIYwnkcn7Y+jOr+5qr2uZqcY6xoSoEfl3NF5z/fi8Kwv6dEdb52opChnDUKvPzhd+81918tKD0R/ZBkK6uLGmT+2zT3DmNltfYcmw4DWCV5aistyg0q0UZ/r0QuPFaysg1C5exlpmp6N7qQlUd3jxc/8Zr25zQY6v+r62J9UxEQpvSr8MUWtrT/QBZllYUVwvjpz4kz1pLzltn6mvB4hhY1FxWgvlZz2ze2ugsn8vPOwv1ui66rdTrehFcWABMa/5f9Um33tf1igkg0NXvZkEDaE3lGyCMLY9gLjVQIQELi5CKsNWuzLT1yabIjOBu7NTK2kJuBbl2Aok3W/sX0e0KZW3yMqXkLG1NIrKV75QXAP3uxC0e9V6a37n6LYTJbE0+Eigv9tW0Nb4ddu5q4wI2NJi04moSVCYegXA5M61wB9WC5V7Momb1qFZjqhy/uEnAn5fnlaColHN6Lqo6B11bKNWccON8+4wuDuspPHQZZee6rxZNKEBnyylrXITe0p/m/fpwPmpCWfDmfX+epFyspZKIOqdi7TxUFf1HLkuB1xiN8fWzds5UkAlTM7HK0m+TYq9RFmerCFljM0CX2rfqmVukymuCRc0P0QiyrF7ictjaLtkJc5WmY44bMneuFfWMCL7iWqaYQgB0jsreb9aiay913Q0jRDozK4XWqRUTR04ApGWRgSzb6wSAgAhB20riyNG3TGfTwtlcM8t9Fm+vifUaP0d/4uHS6rlNhBYZRlYakCoqHAJEISRU6RvdiRJhw9LUWDdHzlOyVPtqi0Xbml+qrd7bBMPUVg7jqljYtdZEQpY+Wt4Fw5cc1uYiUWgehZCYIlWkP7u+85/lKXIJfYyP3i2fRRyXzymWgn67RRTF7N17zIMv7jEcjUhnCSGOfmI2m5UkvUoF6DBgZm3JTaZzU44LR4XlhfFqMVuEq1u1Uz9bUFz6NTmGHFMJgF5j9/9v71yW4whhKHoFPU45//+j3iROD0hZCIFoPyvLzD2LKbs87gd0o4uQhLlQqUOsvjGiu4HKF/SuuDcDfvVzjCf7jhr/JAABPGGJ5NhKDbgkRGU+8QBembUIW8dNPG4zGib+L5YGR3Ntx9sF4M5hcyjahIrC49DnvSev+XYb1/aSlTyRw1a2TpIR2hO/Nk1JIOskkUmb7duH5/2ArV1TvOKVqC6yzr2u57MJz3tCzACvrPDuapdtExsf/YZtGxnc12ObAS0J8Gud2q/4tgD0QO51oYGorcjrywyij1ggn03d13q7AZpOHVvLmXkQbsilKFBpw7qb2jLQRZaYU8NVJRQT3KSgyR1WOmwY4HCJN1XE4zOXYsdtnNahscxbDFp1/14M3Ml5o/bx4JdfjoKyDJCIL/lqHyUWlrfG0GC2Cht384yhlkpJAP5gdO2Qu0CHszMG5zkjAhBldKL/thlvtofZ3d3fCr9gn2nrFBf3fqKV1bJ9pOiXw5N7ejKW+di3kkvQxKMPNDV0eV8c5ut28eWJFYaO2xw1BDWX57E+nxUBlkcMgP7piKswVciIhWnacO/nelrGg2fi3ysiI5vSgEvtxe2eYqwZotY93fv3a5HZMD0yoVtDN4N+If7W+dbn1rc1bayuBunmW/1dBkytOgWgz7+S9/9eEP0jdYkiFUGbXtaCUwE9G2otOKxCjgqp7oGUUlCOUU9SKu5WoU2g7cnfg15R9Qk1vGLRHmPJUXNNuS6wXmBd4V2rs17dbljECzvL+UbYAW0z8pnsrcoeI2id722u87kJQABRt9LM8HssuYd3L3usl6fFx70QyZJKkqxHyD9bfx17V1/3UAeOWxkenwr5cSIErJrBbq94fv6Jl5eXUYDZx4qIkTPz7PyI6bSj4ITvvHOeHe30LQFrvUGP3QsfMbLeHpv6QuwwcbfTV1/GeBxhLK11+FaWbvZa6540USqanjBEKSAXx7Nr7YBgLautt27vy9y1XS4hGsmon31lIufltJq83H6vuwiK56O1tryG5sLC67n5eB7XIWOZ+Y14kj3Rb1vSh6EiJRSM8RVmeMV1+W/E1sOX0OfqkKwxxsxmYXY//hLpAswBZLMfwJYRPJc0gS0M4CrEpiC93LCZ7bYKhhqJQPl4SN5YNVhbnkJP7or3qUJlTRA+SoycSRRxdLn+zX/OiZbbBNv2+FVVm+rQ4E0cO3JN7aOKnrt8e0QNto0dq19yqaGr/dZU6UGKbGXwvuLbdQAJIYQQQsj/wffTRQghhBBCyH8BBSAhhBBCyINBAUgIIYQQ8mBQABJCCCGEPBgUgIQQQgghDwYFICGEEELIg0EBSAghhBDyYFAAEkIIIYQ8GBSAhBBCCCEPxl/oj/YDAioXuQAAAABJRU5ErkJggg==\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, strict=False):\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": [ + { + "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", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "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": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:02<00:00, 25.37it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:01<00:00, 53.25it/s]\n" + ] + }, + { + "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 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, strict=False) 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": { + "base_uri": "https://localhost:8080/" + }, + "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" + ] + }, + { + "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" + }, + "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 new file mode 100644 index 0000000000..78df4f6bc8 --- /dev/null +++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb @@ -0,0 +1,2225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5SSZpZUu0Z4S" + }, + "source": [ + "# DeepLabCut Model Zoo: SuperAnimal models\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", + "# 🦄 SuperAnimal in DeepLabCut PyTorch! 🔥\n", + "\n", + "This notebook demos how to use our SuperAnimal models within DeepLabCut 3.0! Please read more in [Ye et al. Nature Communications 2024](https://www.nature.com/articles/s41467-024-48792-2) about the available SuperAnimal models, and follow along below!\n", + "\n", + "### **Let's get going: install the latest version of 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": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "AjET5cJE5UYM", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "290a589f-a063-4933-d315-e13052ec1024" + }, + "outputs": [], + "source": [ + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5h0vq6E50Z4W" + }, + "source": [ + "**PLEASE, click \"restart runtime\" from the output above before proceeding!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LvnlIvQm0Z4X", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "ef4fd2ed-4569-41d4-b78a-8bf5ae9a0e6b" + }, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from PIL import Image\n", + "\n", + "import deeplabcut\n", + "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions\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" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UeXjmtu40Z4X" + }, + "source": [ + "## Zero-shot Image & Video Inference\n", + "SuperAnimal models are foundation animal pose models. They can be used for zero-shot predictions without further training on the data.\n", + "In this section, we show how to use SuperAnimal models to predict pose from images (given an image folder) and output the predicted images (with pose) into another destination folder." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FvFzntDMxPoL" + }, + "source": [ + "### Zero-shot image inference\n", + "\n", + "If you have a single Image you want to test, upload it here!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NbDsZQfsxPoL" + }, + "source": [ + "#### Upload the images you want to predict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c4yfTj7r0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "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", + "image_path = os.path.abspath(filepath)\n", + "image_name = os.path.splitext(image_path)[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\n", + "# and define `image_path` yourself with right click > copy path on the image:\n", + "#\n", + "# image_path = \"/path/to/my/image.png\"\n", + "# image_name = os.path.splitext(image_path)[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Jashzdjb0Z4Y" + }, + "source": [ + "#### Select a SuperAnimal name and corresponding model architecture\n", + "\n", + "Check Our Docs on [SuperAnimals](https://github.com/DeepLabCut/DeepLabCut/blob/main/docs/ModelZoo.md) to learn more!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uH9LXig90Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "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\"] # fmt: skip # noqa: E501\n", + "\n", + "\n", + "# @markdown ---\n", + "# @markdown What is the maximum number of animals you expect to have in an image\n", + "max_individuals = 3 # @param {type:\"slider\", min:1, max:30, step:1}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OmJtVmHq0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "# Note you need to enter max_individuals correctly to get the correct number of predictions in the image.\n", + "_ = superanimal_analyze_images(\n", + " superanimal_name,\n", + " model_name,\n", + " detector_name,\n", + " image_path,\n", + " max_individuals,\n", + " out_folder=\"/content/\",\n", + " close_figure_after_save=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6VEjHu-00Z4Y" + }, + "source": [ + "### Zero-shot Video Inference\n", + "\n", + "This can be done with or without video adaptation (faster, but not self-supervised fine-tuned on your data!)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qGoAhxZOxPoM" + }, + "source": [ + "#### Upload a video you want to predict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PK3efA0I0Z4Y", + "jupyter": { + "outputs_hidden": true + } + }, + "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", + "video_name = os.path.splitext(video_path)[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\n", + "# and define `video_path` yourself with right click > copy path on the video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JoA-RATSICj_" + }, + "source": [ + "#### Choose the superanimal and the model name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OiRAP9XD0Z4Z", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "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\"] # fmt: skip # noqa: E501\n", + "\n", + "# @markdown ---\n", + "# @markdown What is the maximum number of animals you expect to have in an image\n", + "max_individuals = 3 # @param {type:\"slider\", min:1, max:30, step:1}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Zv3v0QgSJNOg" + }, + "source": [ + "#### Zero-shot Video Inference without video adaptation\n", + "\n", + "The labeled video (and pose predictions for the video) are saved in `\"/content/\"`, with the labeled video name being `{your_video_name}_superanimal_{superanimal_name}_hrnetw32_labeled.mp4`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "poqynL0UJTBp", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "_ = video_inference_superanimal(\n", + " videos=video_path,\n", + " superanimal_name=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " video_adapt=False,\n", + " max_individuals=max_individuals,\n", + " dest_folder=\"/content/\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Z8Z5GSti0Z4Z" + }, + "source": [ + "#### Zero-shot Video Inference with video adaptation (unsupervised)\n", + "\n", + "The labeled video (and pose predictions for the video) are saved in `\"/content/\"`, with the labeled video name being `{your_video_name}_superanimal_{superanimal_name}_hrnetw32_labeled_after_adapt.mp4`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5mhOmtzw0Z4Z", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "_ = video_inference_superanimal(\n", + " videos=[video_path],\n", + " superanimal_name=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " video_adapt=True,\n", + " max_individuals=max_individuals,\n", + " pseudo_threshold=0.1,\n", + " bbox_threshold=0.9,\n", + " detector_epochs=1,\n", + " pose_epochs=1,\n", + " dest_folder=\"/content/\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "br3pwGf40Z4a" + }, + "source": [ + "## Training with SuperAnimal\n", + "\n", + "In this section, we compare different ways to train models in DeepLabCut 3.0, with or without using SuperAnimal-pretrained models.\n", + "You can compare the evaluation results and get a sense of each baseline. We have following baselines:\n", + "\n", + "- ImageNet transfer learning (training without superanimal)\n", + "- SuperAnimal transfer learning (baseline 1)\n", + "- SuperAnimal naive fine-tuning (baseline 2)\n", + "- SuperAnimal memory-replay fine-tuning (baseline3)\n", + "\n", + "This is done on one of your DeepLabCut projects! If you don't have a DeepLabCut project that you can use SuperAnimal models with, you can always using the example openfield dataset [available in the DeepLabCut repository](https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/openfield-Pranav-2018-10-30) or the Tri-Mouse dataset available on [Zenodo](https://zenodo.org/records/5851157)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yPy5VgDDhD6o" + }, + "source": [ + "### Preparing the DeepLabCut Project\n", + "\n", + "First, place your DeepLabCut project folder into you google drive! \"i.e. move the folder named \"Project-YourName-TheDate\" into Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "SXzBBV8ehDR9", + "outputId": "90d61c19-400b-4e5d-8ac9-63680d72cdb5" + }, + "outputs": [], + "source": [ + "# Now, let's link to your GoogleDrive. Run this cell and follow the\n", + "# authorization instructions:\n", + "\n", + "from google.colab import drive\n", + "\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-QmTftBMo4h6" + }, + "source": [ + "You will need to edit the project path in the config.yaml file to be set to your Google Drive link!\n", + "\n", + "Typically, this will be in the format: `/content/drive/MyDrive/yourProjectFolderName`. You can obtain this path by going to the file navigator in the left pane, finding your DeepLabCut project folder, clicking on the vertical `...` next to the folder name and selecting \"Copy path\".\n", + "\n", + "If the `drive` folder is not immediately visible after mounting the drive, refresh the available files!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_iFFEYAB7Uum" + }, + "outputs": [], + "source": [ + "# TODO: Update the `project_path` to be the path of your DeepLabCut project!\n", + "project_path = Path(\"/content/drive/MyDrive/my-project-2024-07-17\")\n", + "config_path = str(project_path / \"config.yaml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HZTG3Eo475w0" + }, + "source": [ + "Then, use the panel below to select the appropriate SuperAnimal model for your project (don't forget to run the cell)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "t8NtCy1Jo0bu" + }, + "outputs": [], + "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\"] # fmt: skip # noqa: E501" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BPvoL9uZ0Z4a" + }, + "source": [ + "### Comparison between different training baselines\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eVmpaLdB0Z4a" + }, + "source": [ + "Definition of data split: the unique combination of training images and testing images.\n", + "We create a data split named split 0. All baselines will share the data split to make fair comparisons.\n", + "- split 0 -> shared by all baselines\n", + "- shuffle 0 (split0) -> imagenet transfer learning\n", + "- shuffle 1 (split0) -> superanimal transfer learning\n", + "- shuffle 2 (split0) -> superanimal naive fine-tuning\n", + "- shuffle 3 (split0) -> superanimal memory-replay fine-tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WofR2jytxPoR" + }, + "source": [ + "### What is the difference between baselines?\n", + "\n", + "**Transfer learning** For canonical task-agnostic transfer learning,\n", + "the encoder learns universal visual features from a large pre-training dataset, and a randomly\n", + "initialized decoder is used to learn the pose from the downstream dataset.\n", + "\n", + "**Fine-tuning** For task aware\n", + "fine-tuning, both encoder and decoder learn task-related visual-pose features\n", + "in the pre-training datasets, and the decoder is fine-tuned to update pose\n", + "priors in downstream datasets. Crucially, the network has pose-estimation-specific\n", + "weights\n", + "\n", + "**ImageNet transfer-learning** The encoder was pre-trained from ImageNet. The decoder is trained from scratch in the downstream tasks\n", + "\n", + "**SuperAnimal transfer-learning** The encoder was pre-trained first from ImageNet, then in pose datasets we colleceted. Then decoder is trained from scratch in downstream tasks.\n", + "\n", + "**SuperAnimal naive fine-tuning** Both the encoder and the decoder were pre-trained in pose datasets we collected. In downstream datasets, we only finetune convolutional channels that correspond to the annotated keypoints in the downstream datasets. This introduces catastrophic forgetting in keypoints that are not annotated in the downstream datasets.\n", + "\n", + "**SuperAnimal memory-replay fine-tuning** If we apply fine-tuning with SuperAnimal without further cares, the models will forget about keypoints that are not annotated in the downstream datasets. To mitigate this, we mix the annotations and zero-shot predictions of SuperAnimal models to create a dataset that 'replays' the memory of the SuperAnimal keypoints.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AgIsUu6v0Z4a", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "imagenet_transfer_learning_shuffle = 0\n", + "superanimal_transfer_learning_shuffle = 1\n", + "superanimal_naive_finetune_shuffle = 2\n", + "superanimal_memory_replay_shuffle = 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "kuKcxM8F0Z4a", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "c7df2943-1e2c-4b85-c20d-8b94a8aabd75" + }, + "outputs": [], + "source": [ + "deeplabcut.create_training_dataset(\n", + " config_path,\n", + " Shuffles=[imagenet_transfer_learning_shuffle],\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_6RncQbr0Z4a" + }, + "source": [ + "### ImageNet transfer learning\n", + "\n", + "Historically, the transfer learning using ImageNet weights strategies assumed no “animal pose task priors” in the pretrained\n", + "model, a paradigm adopted from previous task-agnostic transfer learning.\n", + "\n", + "You can change the number of epochs you want to train for. How long training will take depends on many parameters, including the number of images in your dataset, the resolution of the images, and the number of epochs you train for." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "7ed11ae2a4be462da84ff716e0725af0", + "0f0ed94a863f49b9b85d0a18fa8ce2a5", + "343f2670d37c4bf18859238c3d81d419", + "d104ae21091e4f10a7de18e191b9f04d", + "5dcbd8f3fb6148cca6cfc72b20ce49bd", + "e1675e53ca9a4da8acf6c16fba7a2578", + "3d2996e10f96404baf24d2c4215b75a1", + "b988f87e676840ee98daa3d996c9ddbc", + "1779b84e748b4989a8ed53434c30016f", + "d37cf6fe7c444bc2a2568c3407389ea8", + "2cef5e028d2e40a6bba7400be922d0c2" + ] + }, + "id": "H2z8kM340Z4a", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "75cc2c95-2ac7-4354-9134-4847937e15ce" + }, + "outputs": [], + "source": [ + "# Note we skip the detector training to save time.\n", + "# For Top-Down models, the evaluation is by default using ground-truth bounding\n", + "# boxes. But to train a model that can be used to inference videos and images,\n", + "# you have to set detector_epochs > 0.\n", + "\n", + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=imagenet_transfer_learning_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J-udMck7nDbG" + }, + "source": [ + "Now let's evaluate the performance of our trained models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "TDHMdKz4m_16", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "1d38fb84-7f4c-45d1-dbcd-fd7117ca4dad" + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[imagenet_transfer_learning_shuffle])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0GIFWU-MxPoR" + }, + "source": [ + "### Transfer learning with SuperAnimal weights\n", + "\n", + "First, we prepare training shuffle for transfer-learning with SuperAnimal weights. As we've already create a shuffle with a train/test split that we want to reuse, we use `deeplabcut.create_training_dataset_from_existing_split` to keep the same train/test indices as in the ImageNet transfer learning shuffle.\n", + "\n", + "We specify that we want to initialize the model weights with the selected SuperAnimal model, but without keeping the decoding layers (this is called transfer learning)!\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "wOSdZQtOp8qa", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "ea721606-ea9f-444b-cdae-f62cf0ad30be" + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=False,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_transfer_learning_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3qFxlRHixPoR" + }, + "source": [ + "Then, we launch the training for transfer-learning with SuperAnimal weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "9a996c8dc3b34bc5b8805b3687e22b27", + "d012b421c189412dabeac84cba4164a7", + "1abff22a7c9a416d9166e6b150612171", + "7271412c1f0141649a7300dbce2b003c", + "3c011813d7cb48588a8d236785d9c24f", + "3ea385fe815f4e50a0b81ec299040314", + "fe59f6c5ed7b4e2cb87bb60224acdaba", + "04370d8302c04c5ca6a351383126193f", + "d67c4871543e405fbb576a55f8c9048a", + "a6cb25fa67ef4733a720960b3fc8213c", + "b73b1b64620d492dbc4eaf4bd83ca23a", + "dccbe277cc084ed6aa0b329067b5c69c", + "c8b57833d3f946abae69b84075345a54", + "bee292213d8645618536fcdf6a491d83", + "fbbc8c5b20c7423fb21b74296e0eeb28", + "ff0c737c49624b1ea27588611951fc84", + "42874cdab4be4dc38b0c33775b27d98c", + "e3a185abf8a04edabf32d58bdee10dd1", + "7cdcbbf9cb694dbf949e8b7eea8e7836", + "2ec06260b237411cabd3de7c37e03b1b", + "9f8009429aa34b40a65c998230f20c99", + "2a3abfe7867641db9fbfe3ee76854bf4" + ] + }, + "id": "W60UgRQWqghn", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "18b931b8-98f4-4539-bf82-1910ff5b7f70" + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_transfer_learning_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XzOWKiOixPoR" + }, + "source": [ + "Finally, we evaluate the model obtained by transfer-learning with SuperAnimal weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "jpO3aIAIsWbz", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "30415e5b-8011-4651-af77-a781ea2b5af7" + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[superanimal_transfer_learning_shuffle])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_Es6RR-_0Z4b" + }, + "source": [ + "### Fine-tuning with SuperAnimal (without keeping full SuperAnimal keypoints)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6oo9oJ8XyZrn" + }, + "source": [ + "#### Setup the weight init and dataset\n", + "\n", + "First we do keypoint matching. This steps make it possible to understand the correspondence between the existing annotations and SuperAnimal annotations. This step produces 3 outputs\n", + "- The confusion matrix\n", + "- The conversion table\n", + "- Pseudo predictions over the whole dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fRm62Ji_xPoS" + }, + "source": [ + "#### What is keypoint matching?\n", + "\n", + "Because SuperAnimal models have their pre-defined keypoints that are potentially different from your annotations, we proposed this algorithm to minimize the gap between the model and the dataset. We use our model to perform zero-shot inference on the whole dataset. This gives pairs of predictions and ground truth for every image. Then, we cast the matching between models’ predictions (2D coordinates)\n", + "and ground truth as bipartitematching using the Euclidean distance as the cost between paired of keypoints. We then solve the matching using the Hungarian algorithm. Thus for every image, we end up getting a matching matrix where 1 counts formatch and 0 counts for non-matching. Because the models’ predictions can be noisy from image to image, we average the aforementioned matching matrix across all the images and perform another bipartite matching, resulting in the final keypoint conversion table between the model and the dataset. Note that the quality of thematching will impact the performance\n", + "of the model, especially for zero-shot. In the case where, e.g., the annotation nose is mistakenly converted to keypoint tail and vice versa, the model will have to unlearn the channel that corresponds to nose and tail (see also case study in Mathis et al.)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "vEHeuKSKyjA6", + "jupyter": { + "outputs_hidden": true + }, + "outputId": "5863a81e-e0b9-48c7-f2f9-de14d38e805e" + }, + "outputs": [], + "source": [ + "keypoint_matching(\n", + " config_path,\n", + " superanimal_name,\n", + " model_name,\n", + " detector_name,\n", + " copy_images=True,\n", + ")\n", + "\n", + "conversion_table_path = project_path / \"memory_replay\" / \"conversion_table.csv\"\n", + "confusion_matrix_path = project_path / \"memory_replay\" / \"confusion_matrix.png\"\n", + "\n", + "# You can visualize the pseudo predictions, or do pose embedding clustering etc.\n", + "pseudo_prediction_path = project_path / \"memory_replay\" / \"pseudo_predictions.json\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sA8yyLgs0zoO" + }, + "source": [ + "#### Display the confusion matrix\n", + "\n", + "The x axis lists the keypoints in the existing annotations. The y axis lists the keypoints in SuperAnimal keypoint space. Darker color encodes stronger correspondence between the human annotation and SuperAnimal annotations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "luDxpD9H0zYZ", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "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.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i0QWikYmy_Mj" + }, + "source": [ + "#### Display the conversion table\n", + "The gt columns represents the keypoint names in the existing dataset. The MasterName represents the corresponding keypoints in SuperAnimal keypoint space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CeA-NzDMynYV", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "df = pd.read_csv(conversion_table_path)\n", + "df = df.dropna()\n", + "\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Adding the Conversion Table to your project's `config.yaml` file\n", + "\n", + "Once you've run keypoint matching, you can add the conversion table to your project's `config.yaml` file, and edit it if there are some matches you think are wrong. As an example, for a top-view mouse dataset with 4 bodyparts labeled (`'snout', 'leftear', 'rightear', 'tailbase'`), the conversion table mapping project bodyparts to SuperAnimal bodyparts would be added as:\n", + "\n", + "```yaml\n", + "# Conversion tables to fine-tune SuperAnimal weights\n", + "SuperAnimalConversionTables:\n", + " superanimal_topviewmouse:\n", + " snout: nose\n", + " leftear: left_ear\n", + " rightear: right_ear\n", + " tailbase: tail_base\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "create_conversion_table(\n", + " config=config_path,\n", + " super_animal=superanimal_name,\n", + " project_to_super_animal=read_conversion_table_from_csv(conversion_table_path),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GkfIo8zTxPoS" + }, + "source": [ + "#### Prepare the training shuffle and weight initialization for (naive) fine-tuning with SuperAnimal weights\n", + "\n", + "Then, when you call `build_weight_init` with `with_decoder=True`, the conversion table in your project's `config.yaml` is used to get predictions for the correct bodyparts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xEeM_hrOu6k8", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=True,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_naive_finetune_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gZx6nr-ExPoS" + }, + "source": [ + "#### Launch the training for (naive) fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c3XAr6uRyXOD", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_naive_finetune_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oXuRshzhxPoS" + }, + "source": [ + "#### Evaluate the model obtained by (naive) fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VXfdKS-H2yqw", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(\n", + " config_path,\n", + " Shuffles=[superanimal_naive_finetune_shuffle],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_nUAMlbZ0Z4b" + }, + "source": [ + "### Memory-replay fine-tuning with SuperAnimal (keeping full SuperAnimal keypoints)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n6HPu6RaxPoS" + }, + "source": [ + "**Catastrophic forgetting** describes a\n", + "classic problemin continual learning. Indeed, amodel gradually loses\n", + "its ability to solve previous tasks after it learns to solve new ones.\n", + "Fine-tuning a SuperAnimal models falls into the category of continual\n", + "learning: the downstream dataset defines potentially different\n", + "keypoints than those learned by the models. Thus, the models might\n", + "forget the keypoints they learned and only pick up those defined in the\n", + "target dataset. Here, retraining with the original dataset and the new\n", + "one, is not a feasible option as datasets cannot be easily shared and\n", + "more computational resources would be required.\n", + "To counter that, we treat zero-shot inference of the model as a\n", + "memory buffer that stores knowledge from the original model. When\n", + "we fine-tune a SuperAnimal model, we replace the model predicted\n", + "keypoints with the ground-truth annotations, resulting in hybrid\n", + "learning of old and new knowledge. The quality of the zero-shot predictions\n", + "can vary and we use the confidence of prediction (0.7) as a\n", + "threshold to filter out low-confidence predictions. With the threshold\n", + "set to 1, memory replay fine-tuning becomes naive-fine-tuning." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CSLmjlCIxPoS" + }, + "source": [ + "#### Prepare training shuffle and weight initialization for memory-replay finetuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BKEF76AI0Z4c", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "weight_init = build_weight_init(\n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", + " super_animal=superanimal_name,\n", + " model_name=model_name,\n", + " detector_name=detector_name,\n", + " with_decoder=True,\n", + " memory_replay=True,\n", + ")\n", + "\n", + "deeplabcut.create_training_dataset_from_existing_split(\n", + " config_path,\n", + " from_shuffle=imagenet_transfer_learning_shuffle,\n", + " shuffles=[superanimal_memory_replay_shuffle],\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + " net_type=f\"top_down_{model_name}\",\n", + " detector_type=detector_name,\n", + " weight_init=weight_init,\n", + " userfeedback=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MKwJiIyKxPoT" + }, + "source": [ + "#### Launch the training for memory-replay fine-tuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ru8tIFmD2Mkv", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(\n", + " config_path,\n", + " detector_epochs=0,\n", + " epochs=50,\n", + " save_epochs=10,\n", + " batch_size=64, # if you get a CUDA OOM error when training on a GPU, reduce to 32, 16, ...!\n", + " displayiters=10,\n", + " shuffle=superanimal_memory_replay_shuffle,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i-2MBRDjxPoT" + }, + "source": [ + "#### Evaluate the model obtained by memory-replay finetuning with SuperAnimal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sfMcK3gq8WxZ", + "jupyter": { + "outputs_hidden": true + } + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(config_path, Shuffles=[superanimal_memory_replay_shuffle])" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [ + "UeXjmtu40Z4X", + "FvFzntDMxPoL", + "6VEjHu-00Z4Y" + ], + "gpuType": "T4", + "provenance": [] + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-05-11", + "last_metadata_updated": "2026-05-11", + "last_verified": "2026-05-11", + "verified_for": "3.0.0rc14" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.13" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "04370d8302c04c5ca6a351383126193f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0f0ed94a863f49b9b85d0a18fa8ce2a5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_e1675e53ca9a4da8acf6c16fba7a2578", + "placeholder": "​", + "style": "IPY_MODEL_3d2996e10f96404baf24d2c4215b75a1", + "value": "model.safetensors: 100%" + } + }, + "1779b84e748b4989a8ed53434c30016f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "1abff22a7c9a416d9166e6b150612171": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_04370d8302c04c5ca6a351383126193f", + "max": 159594859, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_d67c4871543e405fbb576a55f8c9048a", + "value": 159594859 + } + }, + "2a3abfe7867641db9fbfe3ee76854bf4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2cef5e028d2e40a6bba7400be922d0c2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2ec06260b237411cabd3de7c37e03b1b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "343f2670d37c4bf18859238c3d81d419": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_b988f87e676840ee98daa3d996c9ddbc", + "max": 165432914, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_1779b84e748b4989a8ed53434c30016f", + "value": 165432914 + } + }, + "3c011813d7cb48588a8d236785d9c24f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3d2996e10f96404baf24d2c4215b75a1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "3ea385fe815f4e50a0b81ec299040314": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "42874cdab4be4dc38b0c33775b27d98c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5dcbd8f3fb6148cca6cfc72b20ce49bd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7271412c1f0141649a7300dbce2b003c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_a6cb25fa67ef4733a720960b3fc8213c", + "placeholder": "​", + "style": "IPY_MODEL_b73b1b64620d492dbc4eaf4bd83ca23a", + "value": " 160M/160M [00:00<00:00, 201MB/s]" + } + }, + "7cdcbbf9cb694dbf949e8b7eea8e7836": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7ed11ae2a4be462da84ff716e0725af0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_0f0ed94a863f49b9b85d0a18fa8ce2a5", + "IPY_MODEL_343f2670d37c4bf18859238c3d81d419", + "IPY_MODEL_d104ae21091e4f10a7de18e191b9f04d" + ], + "layout": "IPY_MODEL_5dcbd8f3fb6148cca6cfc72b20ce49bd" + } + }, + "9a996c8dc3b34bc5b8805b3687e22b27": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_d012b421c189412dabeac84cba4164a7", + "IPY_MODEL_1abff22a7c9a416d9166e6b150612171", + "IPY_MODEL_7271412c1f0141649a7300dbce2b003c" + ], + "layout": "IPY_MODEL_3c011813d7cb48588a8d236785d9c24f" + } + }, + "9f8009429aa34b40a65c998230f20c99": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a6cb25fa67ef4733a720960b3fc8213c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b73b1b64620d492dbc4eaf4bd83ca23a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "b988f87e676840ee98daa3d996c9ddbc": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bee292213d8645618536fcdf6a491d83": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "FloatProgressModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "FloatProgressModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ProgressView", + "bar_style": "success", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_7cdcbbf9cb694dbf949e8b7eea8e7836", + "max": 517816013, + "min": 0, + "orientation": "horizontal", + "style": "IPY_MODEL_2ec06260b237411cabd3de7c37e03b1b", + "value": 517816013 + } + }, + "c8b57833d3f946abae69b84075345a54": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_42874cdab4be4dc38b0c33775b27d98c", + "placeholder": "​", + "style": "IPY_MODEL_e3a185abf8a04edabf32d58bdee10dd1", + "value": "detector.pt: 100%" + } + }, + "d012b421c189412dabeac84cba4164a7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_3ea385fe815f4e50a0b81ec299040314", + "placeholder": "​", + "style": "IPY_MODEL_fe59f6c5ed7b4e2cb87bb60224acdaba", + "value": "pose_model.pth: 100%" + } + }, + "d104ae21091e4f10a7de18e191b9f04d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_d37cf6fe7c444bc2a2568c3407389ea8", + "placeholder": "​", + "style": "IPY_MODEL_2cef5e028d2e40a6bba7400be922d0c2", + "value": " 165M/165M [00:04<00:00, 41.1MB/s]" + } + }, + "d37cf6fe7c444bc2a2568c3407389ea8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d67c4871543e405fbb576a55f8c9048a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ProgressStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ProgressStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "bar_color": null, + "description_width": "" + } + }, + "dccbe277cc084ed6aa0b329067b5c69c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_c8b57833d3f946abae69b84075345a54", + "IPY_MODEL_bee292213d8645618536fcdf6a491d83", + "IPY_MODEL_fbbc8c5b20c7423fb21b74296e0eeb28" + ], + "layout": "IPY_MODEL_ff0c737c49624b1ea27588611951fc84" + } + }, + "e1675e53ca9a4da8acf6c16fba7a2578": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3a185abf8a04edabf32d58bdee10dd1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "fbbc8c5b20c7423fb21b74296e0eeb28": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_9f8009429aa34b40a65c998230f20c99", + "placeholder": "​", + "style": "IPY_MODEL_2a3abfe7867641db9fbfe3ee76854bf4", + "value": " 518M/518M [00:05<00:00, 101MB/s]" + } + }, + "fe59f6c5ed7b4e2cb87bb60224acdaba": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "ff0c737c49624b1ea27588611951fc84": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb new file mode 100644 index 0000000000..bf905df7b4 --- /dev/null +++ b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb @@ -0,0 +1,435 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "RK255E7YoEIt" + }, + "source": [ + "# DeepLabCut for your standard (single animal) projects!\n", + "\n", + "Some useful links:\n", + "\n", + "- [DeepLabCut's GitHub: github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", + "\n", + "\n", + "This notebook illustrates how to use the cloud to:\n", + "- create a training set\n", + "- train a network\n", + "- evaluate a network\n", + "- create simple quality check plots\n", + "- analyze novel videos!\n", + "\n", + "### This notebook assumes you already have a project folder with labeled data! \n", + "\n", + "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", + "\n", + "This shows the most simple code to do so, but many of the functions have additional features, so please check out the [documentation](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html) & the protocol paper!\n", + "\n", + "> Nath, T., Mathis, A., Chen, A. C., Patel, A., Bethge, M., & Mathis, M. W. (2019). \n", + "> **Using DeepLabCut for 3D markerless pose estimation across species and behaviors.** \n", + "> *Nature Protocols, 14*(7), 2152–2176. \n", + "> https://doi.org/10.1038/s41596-019-0176-0\n", + "\n", + "Pre-print: https://www.biorxiv.org/content/biorxiv/early/2018/11/24/476531.full.pdf\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "txoddlM8hLKm" + }, + "source": [ + "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n", + "\n", + "As the COLAB environments were updated to CUDA 12.X and Python 3.11, we need to install DeepLabCut and TensorFlow in a distinct way to get TensorFlow to connect to the GPU." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "25wSj6TlVclR" + }, + "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": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oTwAcbq2-FZz", + "outputId": "9cfd8dcf-a0a8-4801-ed1d-fbcd5ec056af" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" + ] + } + ], + "source": [ + "import deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "cQ-nlTkri4HZ" + }, + "source": [ + "## Link your Google Drive (with your labeled data, or the demo data):\n", + "\n", + "### First, place your project folder into you google drive! \"i.e. move the folder named \"Project-YourName-TheDate\" into google drive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "KS4Q4UkR9rgG" + }, + "outputs": [], + "source": [ + "# Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", + "# (We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", + "\n", + "from google.colab import drive\n", + "\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Frnj1RVDyEqs" + }, + "source": [ + "YOU WILL NEED TO EDIT THE PROJECT PATH **in the config.yaml file** TO BE SET TO YOUR GOOGLE DRIVE LINK!\n", + "\n", + "Typically, this will be: `/content/drive/My Drive/yourProjectFolderName`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "vhENAlQnFENJ" + }, + "outputs": [], + "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", + "\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", + "videofile_path = [f\"/content/drive/My Drive/{project_folder_name}/videos/\"]\n", + "print(videofile_path)\n", + "\n", + "# The prediction files and labeled videos will be saved in this `labeled-videos` folder\n", + "# in your project folder; if you want them elsewhere, you can edit this;\n", + "# 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", + "path_config_file = f\"/content/drive/My Drive/{project_folder_name}/config.yaml\"\n", + "print(path_config_file)\n", + "\n", + "# This creates a path variable that links to your Google Drive project" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "xNi9s1dboEJN" + }, + "source": [ + "## Create a training dataset:\n", + "\n", + "### You must do this step inside of Colab\n", + "\n", + "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", + "\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pytorch_config.yaml**.\n", + "\n", + "Now it is the time to start training the network!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "eMeUwgxPoEJP", + "scrolled": true + }, + "outputs": [], + "source": [ + "# 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(path_config_file, net_type=\"resnet_50\", engine=deeplabcut.Engine.PYTORCH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "c4FczXGDoEJU" + }, + "source": [ + "## Start training:\n", + "This function trains the network for a specific shuffle of the training dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "_pOvDq_2oEJW" + }, + "outputs": [], + "source": [ + "# Let's also change the display and save_epochs just in case Colab takes away\n", + "# the GPU... If that happens, you can reload from a saved point using the\n", + "# `snapshot_path` argument to `deeplabcut.train_network`:\n", + "# deeplabcut.train_network(..., snapshot_path=\"/content/.../snapshot-050.pt\")\n", + "\n", + "# Typically, you want to train to ~200 epochs. We set the batch size to 8 to\n", + "# utilize the GPU's capabilities.\n", + "\n", + "# More info and there are more things you can set:\n", + "# https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html#g-train-the-network\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=5,\n", + " epochs=200,\n", + " batch_size=8,\n", + ")\n", + "\n", + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "RiDwIVf5-3H_" + }, + "source": [ + "Note, that **when you hit \"STOP\" you will get a `KeyboardInterrupt` \"error\"! No worries! :)**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "xZygsb2DoEJc" + }, + "source": [ + "## Start evaluating:\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "nv4zlbrnoEJg" + }, + "outputs": [], + "source": [ + "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!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "BaLBl3TQtrfB" + }, + "source": [ + "## There is an optional refinement step you can do outside of Colab:\n", + "- if your pixel errors are not low enough, please check out the protocol guide on how to refine your network!\n", + "- You will need to adjust the labels **outside of Colab!** We recommend coming back to train and analyze videos... \n", + "- Please see the repo and protocol instructions on how to refine your data!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OVFLSKKfoEJk" + }, + "source": [ + "## Start Analyzing videos: \n", + "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", + "\n", + "The results are stored in hd5 file in the same directory where the video resides. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Y_LZiS_0oEJl" + }, + "outputs": [], + "source": [ + "deeplabcut.analyze_videos(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8GTiuJESoEKH" + }, + "source": [ + "## Plot the trajectories of the analyzed videos:\n", + "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "gX21zZbXoEKJ" + }, + "outputs": [], + "source": [ + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "pqaCw15v8EmB" + }, + "source": [ + "Now you can look at the plot-poses file and check the \"plot-likelihood.png\" might want to change the \"p-cutoff\" in the config.yaml file so that you have only high confidnece points plotted in the video. i.e. ~0.8 or 0.9. The current default is 0.4. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "pCrUvQIvoEKD" + }, + "source": [ + "## Create labeled video:\n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "6aDF7Q7KoEKE" + }, + "outputs": [], + "source": [ + "deeplabcut.create_labeled_video(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " destfolder=destfolder,\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "Copy of latest_Colab_TrainNetwork_VideoAnalysis.ipynb", + "provenance": [], + "toc_visible": true + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-05-11", + "last_metadata_updated": "2026-05-11", + "last_verified": "2026-05-11", + "verified_for": "3.0.0rc14", + "notes": "This notebook demos the primary generic/PyTorch API for single-animal DeepLabCut projects. Note that it is a bit outdated and may need revisions after dropping TensorFlow support. Also it does not reflect any of the planned refactors currently in the works (e.g. structured configs, keypoints)." + }, + "kernelspec": { + "display_name": "benchmarking", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb new file mode 100644 index 0000000000..702612b8e4 --- /dev/null +++ b/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RK255E7YoEIt" + }, + "source": [ + "# DeepLabCut for your multi-animal projects!\n", + "\n", + "Some useful links:\n", + "\n", + "- [DeepLabCut's GitHub: github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Multi-Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html)\n", + "\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628180434489-T0RIWEJJU0FJVOT6FNVD/maDLC.png?format=800w)\n", + "\n", + "This notebook illustrates how to, for multi-animal projects, use the cloud-based GPU to:\n", + "- create a multi-animal training set\n", + "- train a network\n", + "- evaluate a network\n", + "- analyze novel videos\n", + "- assemble animals and tracklets\n", + "- create quality check plots!\n", + "\n", + "### This notebook assumes you already have a DLC project folder with labeled data and you uploaded it to your own Google Drive.\n", + "\n", + "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", + "\n", + "This shows the most simple code to do so, but many of the functions have additional features, so please check out the docs on GitHub. We also recommend checking out our preprint, which covers the science of maDLC\n", + "\n", + "**Lauer et al 2021:** https://www.biorxiv.org/content/10.1101/2021.04.30.442096v1\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "txoddlM8hLKm" + }, + "source": [ + "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n", + "\n", + "Note: Colab uses Python 3.12, which is not supported by older versions of DeepLabCut. We need to install a recent DeepLabCut version." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oTwAcbq2-FZz", + "outputId": "9cfd8dcf-a0a8-4801-ed1d-fbcd5ec056af" + }, + "outputs": [], + "source": [ + "import deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cQ-nlTkri4HZ" + }, + "source": [ + "## Link your Google Drive (with your labeled data):\n", + "\n", + "- This code assumes you locally installed DeepLabCut, created a project, extracted and labeled frames. Be sure to \"check Labels\" to confirm you are happy with your data. As, these frames are the only thing that is used to train your network. 💪 You can find all the docs to do this here: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", + "- Next, place your DLC project folder into you Google Drive- i.e., copy the folder named \"Project-YourName-TheDate\" into Google Drive.\n", + "- Then, click run on the cell below to link this notebook to your Google Drive:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KS4Q4UkR9rgG" + }, + "outputs": [], + "source": [ + "# Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", + "# (We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", + "\n", + "from google.colab import drive\n", + "\n", + "drive.mount(\"/content/drive\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Frnj1RVDyEqs" + }, + "source": [ + "## Next, edit the few items below, and click run:\n", + "\n", + "YOU WILL NEED TO EDIT THE PROJECT PATH **in the `config.yaml` file** TO BE SET TO YOUR GOOGLE DRIVE LINK! Typically, this will be: `/content/drive/My Drive/yourProjectFolderName`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vhENAlQnFENJ" + }, + "outputs": [], + "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", + "\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", + "videofile_path = [f\"/content/drive/My Drive/{project_folder_name}/videos/\"]\n", + "print(videofile_path)\n", + "\n", + "# The prediction files and labeled videos will be saved in this `labeled-videos` folder\n", + "# in your project folder; if you want them elsewhere, you can edit this;\n", + "# 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", + "path_config_file = f\"/content/drive/My Drive/{project_folder_name}/config.yaml\"\n", + "print(path_config_file)\n", + "\n", + "# This creates a path variable that links to your Google Drive project" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xNi9s1dboEJN" + }, + "source": [ + "## Create a multi-animal training dataset:\n", + "\n", + "- more info can be [found in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#create-training-dataset)\n", + "- please check the text below, edit if needed, and then click run (this can take some time):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-AAYSoW313me" + }, + "outputs": [], + "source": [ + "# OPTIONAL LEARNING: did you know you can check what each function does by running with a ?\n", + "deeplabcut.create_multianimaltraining_dataset?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eMeUwgxPoEJP", + "scrolled": true + }, + "outputs": [], + "source": [ + "# ATTENTION:\n", + "# Which shuffle do you want to create and train?\n", + "shuffle = 1 # Edit if needed; 1 is the default.\n", + "\n", + "deeplabcut.create_multianimaltraining_dataset(\n", + " path_config_file,\n", + " Shuffles=[shuffle],\n", + " net_type=\"dlcrnet_ms5\",\n", + " engine=deeplabcut.Engine.PYTORCH,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c4FczXGDoEJU" + }, + "source": [ + "## Start training:\n", + "This function trains the network for a specific shuffle of the training dataset. More info can be found [in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#train-the-network)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_pOvDq_2oEJW" + }, + "outputs": [], + "source": [ + "# Let's also change the display and save_epochs just in case Colab takes away\n", + "# the GPU... If that happens, you can reload from a saved point using the\n", + "# `snapshot_path` argument to `deeplabcut.train_network`:\n", + "# deeplabcut.train_network(..., snapshot_path=\"/content/.../snapshot-050.pt\")\n", + "\n", + "# Typically, you want to train to ~200 epochs. We set the batch size to 8 to\n", + "# utilize the GPU's capabilities.\n", + "\n", + "# More info and there are more things you can set:\n", + "# https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html#g-train-the-network\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=shuffle,\n", + " save_epochs=5,\n", + " epochs=200,\n", + " batch_size=8,\n", + ")\n", + "\n", + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RiDwIVf5-3H_" + }, + "source": [ + "Note, that **when you hit \"STOP\" you will get a `KeyboardInterrupt` \"error\"! No worries! :)**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xZygsb2DoEJc" + }, + "source": [ + "## Start evaluating: \n", + "\n", + "- First, we evaluate the pose estimation performance.\n", + "- This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images) and stores the results as .5 and .csv file in a subdirectory under **evaluation-results-pytorch**\n", + "- If the scoremaps do not look accurate, don't proceed to tracklet assembly; please consider (1) adding more data, (2) adding more bodyparts!\n", + "- More info can be [found in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#evaluate-the-trained-network)\n", + "\n", + "Here is an example of what you'd aim to see before proceeding:\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1590535809087-X655WY9W1MW1MY1I7DHE/ke17ZwdGBToddI8pDm48kBoswZhKnUtAF7-bTXgw67EUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYxCRW4BPu10St3TBAUQYVKc5tTP1cnANTUwNNPnYFjIp6XbP9N1GxIgAkxvBVqt0UvLpPHYwvNQTwHg8f_Zu8ZF/evaluation.png?format=1000w)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nv4zlbrnoEJg" + }, + "outputs": [], + "source": [ + "# Let's evaluate first:\n", + "deeplabcut.evaluate_network(path_config_file, Shuffles=[shuffle], plotting=True)\n", + "\n", + "# plot a few scoremaps:\n", + "deeplabcut.extract_save_all_maps(path_config_file, shuffle=shuffle, Indices=[0, 1, 2, 3])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fYlGbloolDU2" + }, + "source": [ + "IF these images, numbers, and maps do not look good, do not proceed. You should increase the diversity and number of frames you label, and re-create a training dataset and re-train! " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OVFLSKKfoEJk" + }, + "source": [ + "## Start Analyzing videos: \n", + "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", + "\n", + "The results are stored in a pickle file in the same directory where the video resides. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Y_LZiS_0oEJl" + }, + "outputs": [], + "source": [ + "print(\"Start Analyzing my video(s)!\")\n", + "#EDIT OPTION: which video(s) do you want to analyze? You can pass a path or a folder:\n", + "# currently, if you run \"as is\" it assumes you have a video in the DLC project video folder!\n", + "\n", + "deeplabcut.analyze_videos(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " videotype=video_type,\n", + " auto_track=False,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "91xBLOcBzGxo" + }, + "source": [ + "Optional: Now you have the option to check the raw detections before animals are tracked. To do so, pass a video path:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "65mWwX5bTc5C" + }, + "outputs": [], + "source": [ + "##### PROTIP: #####\n", + "## look at the output video; if the pose estimation (i.e. key points)\n", + "## don't look good, don't proceed with tracking - add more data to your training set and re-train!\n", + "\n", + "# EDIT: let's check a specific video (PLEASE EDIT VIDEO PATH):\n", + "specific_videofile = \"/content/drive/MyDrive/DeepLabCut_maDLC_DemoData/MontBlanc-Daniel-2019-12-16/videos/short.mov\"\n", + "\n", + "# Don't edit:\n", + "deeplabcut.create_video_with_all_detections(\n", + " path_config_file, [specific_videofile], shuffle=shuffle, destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3-OgTJ0Lz20e" + }, + "source": [ + "If the resulting video (ends in full.mp4) is not good, we highly recommend adding more data and training again. See [here, in the docs](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html#decision-break-point)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PxRLS2_-r55K" + }, + "source": [ + "## Next, we will assemble animals using our data-driven optimal graph method:\n", + "\n", + "During video analysis, animals are assembled using the optimal graph, which matches the \"data-driven\" method from our paper (Figure adapted from Lauer et al. 2021)\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1626266017809-XO6NX84QB4FBAZGOTCEY/fig3.jpg?format=400w)\n", + "\n", + "The optimal graph is computed when `evaluate_network` - so make sure you don't skip that step!\n", + "\n", + "**Note**: you can set the number of animals you expect to see, so check, edit, then click run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zIvXM7TXIs-U" + }, + "outputs": [], + "source": [ + "#Check and edit:\n", + "num_animals = 4 # How many animals do you expect to find?\n", + "track_type= \"box\" # box, skeleton, ellipse\n", + "#-- ellipse is recommended, unless you have a single-point MA project, then use BOX!\n", + "\n", + "# Optional:\n", + "# imagine you tracked a point that is not useful for assembly,\n", + "# like a tail tip that is far from the body, consider dropping it for this step (it's still used later)!\n", + "# To drop it, uncomment the next line TWO lines and add your parts(s):\n", + "\n", + "# bodypart= 'Tail_end'\n", + "# deeplabcut.convert_detections2tracklets(path_config_file, videofile_path, videotype=VideoType, shuffle=shuffle, overwrite=True, ignore_bodyparts=[bodypart])\n", + "\n", + "# OR don't drop, just click RUN:\n", + "deeplabcut.convert_detections2tracklets(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + " overwrite=True,\n", + ")\n", + "\n", + "deeplabcut.stitch_tracklets(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " n_tracks=num_animals,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TqbAnyfL0Q7h" + }, + "source": [ + "Now let's filter the data to remove any small jitter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a6izVWX8sdzL" + }, + "outputs": [], + "source": [ + "deeplabcut.filterpredictions(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " videotype=video_type,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Zk4xGb8Ftf3B" + }, + "source": [ + "## Create plots of your trajectories:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gX21zZbXoEKJ" + }, + "outputs": [], + "source": [ + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " videofile_path,\n", + " videotype=video_type,\n", + " shuffle=shuffle,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pqaCw15v8EmB" + }, + "source": [ + "Now you can look at the plot-poses file and check the \"plot-likelihood.png\" might want to change the \"p-cutoff\" in the config.yaml file so that you have only high confidnece points plotted in the video. i.e. ~0.8 or 0.9. The current default is 0.4. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pCrUvQIvoEKD" + }, + "source": [ + "## Create labeled video:\n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6aDF7Q7KoEKE" + }, + "outputs": [], + "source": [ + "deeplabcut.create_labeled_video(\n", + " path_config_file,\n", + " videofile_path,\n", + " shuffle=shuffle,\n", + " color_by=\"individual\",\n", + " videotype=video_type,\n", + " save_frames=False,\n", + " filtered=True,\n", + " track_method=track_type,\n", + " destfolder=destfolder,\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb", + "provenance": [] + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2026-02-10", + "last_metadata_updated": "2026-05-11", + "last_verified": "2026-05-11", + "verified_for": "3.0.0rc14", + "notes": "This notebook demos the primary generic/PyTorch API for multi-animal DeepLabCut (maDLC) projects. Note that it may need revisions after dropping TensorFlow support. And does not reflect any of the planned refactors currently in the works (e.g. structured configs, keypoints)." + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/COLAB/COLAB_transformer_reID.ipynb b/examples/COLAB/COLAB_transformer_reID.ipynb new file mode 100644 index 0000000000..12eb4f89ea --- /dev/null +++ b/examples/COLAB/COLAB_transformer_reID.ipynb @@ -0,0 +1,634 @@ +{ + "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 os\n", + "\n", + "import deeplabcut" + ] + }, + { + "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": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading demo-me-2021-07-14.zip...\n" + ] + } + ], + "source": [ + "# Download our demo project:\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", + " 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": [ + { + "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], 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": [ + { + "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", + ")" + ] + }, + { + "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": [ + { + "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": [ + { + "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", + ")" + ] + }, + { + "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": { + "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", + ")" + ] + }, + { + "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", + ")" + ] + } + ], + "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" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/COLAB_DEMO_mouse_openfield.ipynb b/examples/COLAB_DEMO_mouse_openfield.ipynb deleted file mode 100644 index b2fcd6c73f..0000000000 --- a/examples/COLAB_DEMO_mouse_openfield.ipynb +++ /dev/null @@ -1,340 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "accelerator": "GPU", - "colab": { - "name": "Colab_DEMO_mouse_openfield.ipynb", - "provenance": [], - "collapsed_sections": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernelspec": { - "display_name": "Python [default]", - "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.6.9" - } - }, - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "RK255E7YoEIt" - }, - "source": [ - "# DeepLabCut Toolbox - Colab Demo on topview mouse data\n", - "https://github.com/AlexEMG/DeepLabCut\n", - "\n", - "**new:** no need to download from GitHub and link to Google Drive! Now you can directly use our demo data!\n", - "\n", - "Nath\\*, Mathis\\* et al. *Using DeepLabCut for markerless3D pose estimation during behavior across species. Nature Protocols, 2019 \n", - "\n", - "This notebook demonstrates the necessary steps to use DeepLabCut on our demo data. We provide a sub-set of the mouse data from Mathis et al, 2018 Nature Neuroscience.\n", - "\n", - "This demo notebook mostly shows the most simple code to train and evaluate your model, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", - "\n", - "This notebook illustrates how to use the cloud to:\n", - "\n", - "- load demo data\n", - "- create a training set\n", - "- train a network\n", - "- evaluate a network\n", - "- analyze a novel video" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "txoddlM8hLKm" - }, - "source": [ - "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"" - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "Ew6r4hotoQjt", - "colab": {} - }, - "source": [ - "# Clone the entire deeplabcut repo so we can use the demo data:\n", - "!git clone -l -s git://github.com/AlexEMG/DeepLabCut.git cloned-DLC-repo\n", - "%cd cloned-DLC-repo\n", - "!ls" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "yDaY78dFoxyD", - "colab": {} - }, - "source": [ - "%cd /content/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30\n", - "!ls" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "q23BzhA6CXxu", - "colab": {} - }, - "source": [ - "#(this will take a few minutes to install all the dependences!)\n", - "\n", - "!pip install deeplabcut" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "XymV_Hnlp1OJ" - }, - "source": [ - "## PLEASE, click \"restart runtime\" from the output above before proceeding! " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "dC2AwU7FcOPC", - "colab_type": "code", - "colab": {} - }, - "source": [ - "# Use TensorFlow 1.x:\n", - "%tensorflow_version 1.x" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "sXufoX6INe6w", - "colab": {} - }, - "source": [ - "#GUIs don't work on the cloud, so we will supress wxPython: \n", - "import os\n", - "os.environ[\"DLClight\"]=\"True\"\n", - "\n", - "import deeplabcut" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "Z7ZlDr3wV4D1", - "colab": {} - }, - "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", - "\n", - "# Loading example data set:\n", - "deeplabcut.load_demo_data(path_config_file)" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "c4FczXGDoEJU" - }, - "source": [ - "## Start training:\n", - "This function trains the network for a specific shuffle of the training dataset. " - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "_pOvDq_2oEJW", - "colab": {} - }, - "source": [ - "#let's also change the display and save_iters just in case Colab takes away the GPU... \n", - "#if that happens, you can reload from a saved point. Typically, you want to train to 200,000 + iterations.\n", - "#more info and there are more things you can set: https://github.com/AlexEMG/DeepLabCut/blob/master/docs/functionDetails.md#g-train-the-network\n", - "\n", - "deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10,saveiters=100)\n", - "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 1.03M iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry...." - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "RiDwIVf5-3H_" - }, - "source": [ - "We recommend you run this for ~1,000 iterations, just as a demo. This should take around 20 min. Note, that **when you hit \"STOP\" you will get a KeyInterrupt \"error\"! No worries! :)**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "xZygsb2DoEJc" - }, - "source": [ - "## Start evaluating:\n", - "This funtion evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "nv4zlbrnoEJg", - "colab": {} - }, - "source": [ - "%matplotlib notebook\n", - "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 good as the labeler, so be sure your labels are good!" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "oxy5JG-kYKF4" - }, - "source": [ - "**Check the images**:\n", - "You can go look in the newly created \"evalutaion-results\" folder at the images. At around 3500 iterations, the error is ~3 pixels (but this can vary on how your demo data was split for training)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "OVFLSKKfoEJk" - }, - "source": [ - "## Start Analyzing videos: \n", - "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", - "\n", - "The results are stored in hd5 file in the same directory where the video resides. \n", - "\n", - "**On the demo data, this should take around ~ 3 min! (The demo frames are 640x480, which should run around 35 FPS on the google-provided GPU)**" - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "Y_LZiS_0oEJl", - "colab": {} - }, - "source": [ - "videofile_path = ['/content/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4'] #Enter the list of videos to analyze.\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype='.mp4')" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "pCrUvQIvoEKD" - }, - "source": [ - "## Create labeled video:\n", - "This funtion is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. This should run around 215 FPS on the demo video!" - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "6aDF7Q7KoEKE", - "colab": {} - }, - "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "8GTiuJESoEKH" - }, - "source": [ - "## Plot the trajectories of the analyzed videos:\n", - "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color." - ] - }, - { - "cell_type": "code", - "metadata": { - "colab_type": "code", - "id": "gX21zZbXoEKJ", - "colab": {} - }, - "source": [ - "deeplabcut.plot_trajectories(path_config_file,videofile_path)" - ], - "execution_count": 0, - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/examples/COLAB_DLC_ModelZoo.ipynb b/examples/COLAB_DLC_ModelZoo.ipynb deleted file mode 100644 index a09b24f43e..0000000000 --- a/examples/COLAB_DLC_ModelZoo.ipynb +++ /dev/null @@ -1,320 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "accelerator": "GPU", - "colab": { - "name": "Copy of COLAB_DLC_ModelZoo.ipynb", - "provenance": [], - "collapsed_sections": [], - "toc_visible": true, - "include_colab_link": true - }, - "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": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RK255E7YoEIt" - }, - "source": [ - "# **DeepLabCut Model Zoo!**\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 littel 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\"*" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "q23BzhA6CXxu" - }, - "source": [ - "#click the play icon (this will take a few minutes to install all the dependences!)\n", - "!pip install deeplabcut\n", - "%reload_ext numpy\n", - "%reload_ext matplotlib\n", - "%reload_ext mpl_toolkits" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "Jw4xz2xy2dAo" - }, - "source": [ - "# Use TensorFlow 1.x:\n", - "%tensorflow_version 1.x" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ZT4PwGSbYQEO" - }, - "source": [ - "## Now let's set the backend & import the DeepLabCut package:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "bvoiWefrYQEP" - }, - "source": [ - "#GUIs don't work on the cloud, so we supress them:\n", - "import os\n", - "os.environ[\"DLClight\"]=\"True\"\n", - "\n", - "# stifle tensorflow warnings, like we get it already.\n", - "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\n", - "\n", - "import deeplabcut" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "syweXs88tyuO" - }, - "source": [ - "## Next, run the cell below to upload your video file from your computer:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "7eqEZYs_CaLy" - }, - "source": [ - "from google.colab import files\n", - "uploaded = files.upload()\n", - "for fn in uploaded.keys():\n", - " print('User uploaded file \"{video_path}\" with length {length} bytes'.format(\n", - " video_path=fn, length=len(uploaded[fn])))\n", - " video_path = fn" - ], - "execution_count": null, - "outputs": [] - }, - { - "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", - "metadata": { - "id": "Ih0t7lUjYQEd" - }, - "source": [ - "import ipywidgets as widgets\n", - "from IPython.display import display\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)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "UV0QXswGCFrI" - }, - "source": [ - "ProjectFolderName = 'myDLC_modelZoo'\n", - "YourName = 'teamDLC'\n", - "model2use = model_selection.value\n", - "videotype = os.path.splitext(video_path)[-1].lstrip('.') #or MOV, or avi, whatever you uploaded!" - ], - "execution_count": null, - "outputs": [] - }, - { - "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", - "metadata": { - "id": "WpAX3BKY94e0" - }, - "source": [ - "deeplabcut.DownSampleVideo(video_path, width=300)\n", - "\n", - "import os\n", - "from pathlib import Path\n", - "video_path=os.path.join(str(Path(video_path).stem)+'downsampled.'+videotype)\n", - "print(video_path)" - ], - "execution_count": null, - "outputs": [] - }, - { - "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", - "metadata": { - "id": "T9MGgAdIFKPY" - }, - "source": [ - "path_config_file = deeplabcut.create_pretrained_project(ProjectFolderName, YourName, video_path, videotype=videotype, \n", - " model=model2use, analyzevideo=True, createlabeledvideo=True, copy_videos=True) #must leave copy_videos=True" - ], - "execution_count": null, - "outputs": [] - }, - { - "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", - "metadata": { - "id": "yGLNVK1q6rIp" - }, - "source": [ - "# Updating the plotting within the config.yaml file (without opening it ;):\n", - "\n", - "#dotsize: size of the dots!\n", - "#colormap: any matplotlib colormap!\n", - "#pcutoff: the higher the more conservative the plotting!\n", - "\n", - "config_path = path_config_file[0]\n", - "edits = {'dotsize': 7,\n", - " 'colormap': 'spring',\n", - " 'pcutoff': 0.5}\n", - "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "Vlc0wZgB7R5e" - }, - "source": [ - "# re-create the labeled video (first you will need to delete in the folder to the LEFT!):\n", - "from datetime import datetime\n", - "#The name of the project you created:\n", - "project_folder_name = '-'.join([ProjectFolderName, YourName, datetime.now().strftime('%Y-%m-%d')])\n", - "\n", - "full_video_path = videofile_path = ['/content/'+project_folder_name+'/videos/'+video_path]\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)" - ], - "execution_count": null, - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/examples/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb deleted file mode 100644 index f53aa7f1b2..0000000000 --- a/examples/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb +++ /dev/null @@ -1,428 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "name": "Copy of latest_Colab_TrainNetwork_VideoAnalysis.ipynb", - "provenance": [], - "collapsed_sections": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "accelerator": "GPU" - }, - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RK255E7YoEIt", - "colab_type": "text" - }, - "source": [ - "# DeepLabCut Toolbox - Colab\n", - "https://github.com/AlexEMG/DeepLabCut\n", - "\n", - "This notebook illustrates how to use the cloud to:\n", - "- create a training set\n", - "- train a network\n", - "- evaluate a network\n", - "- create simple quality check plots\n", - "- analyze novel videos!\n", - "\n", - "###This notebook assumes you already have a project folder with labeled data! \n", - "\n", - "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", - "\n", - "This shows the most simple code to do so, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", - "\n", - "Nath\\*, Mathis\\* et al.: Using DeepLabCut for markerless pose estimation during behavior across species. Nature Protocols, 2019.\n", - "\n", - "\n", - "Paper: https://www.nature.com/articles/s41596-019-0176-0\n", - "\n", - "Pre-print: https://www.biorxiv.org/content/biorxiv/early/2018/11/24/476531.full.pdf\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "txoddlM8hLKm", - "colab_type": "text" - }, - "source": [ - "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "q23BzhA6CXxu", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#(this will take a few minutes to install all the dependences!)\n", - "!pip install deeplabcut" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "25wSj6TlVclR", - "colab_type": "text" - }, - "source": [ - "**(Be sure to click \"RESTART RUNTIME\" is it is displayed above above before moving on !)**" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Y36K4Eux3h-X", - "colab_type": "code", - "colab": {} - }, - "source": [ - "# Use TensorFlow 1.x:\n", - "%tensorflow_version 1.x" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "cQ-nlTkri4HZ", - "colab_type": "text" - }, - "source": [ - "## Link your Google Drive (with your labeled data, or the demo data):\n", - "\n", - "### First, place your porject folder into you google drive! \"i.e. move the folder named \"Project-YourName-TheDate\" into google drive." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "KS4Q4UkR9rgG", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", - "#(We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", - "\n", - "from google.colab import drive\n", - "drive.mount('/content/drive')" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Frnj1RVDyEqs", - "colab_type": "text" - }, - "source": [ - "YOU WILL NEED TO EDIT THE PROJECT PATH **in the config.yaml file** TO BE SET TO YOUR GOOGLE DRIVE LINK!\n", - "\n", - "Typically, this will be: /content/drive/My Drive/yourProjectFolderName\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "vhENAlQnFENJ", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#Setup your project variables:\n", - "# PLEASE EDIT THESE:\n", - " \n", - "ProjectFolderName = 'myproject-teamDLC-2020-03-29'\n", - "VideoType = 'mp4' \n", - "\n", - "#don't edit these:\n", - "videofile_path = ['/content/drive/My Drive/'+ProjectFolderName+'/videos/'] #Enter the list of videos or folder to analyze.\n", - "videofile_path" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "sXufoX6INe6w", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#GUIs don't work on the cloud, so label your data locally on your computer! This will suppress the GUI support\n", - "import os\n", - "os.environ[\"DLClight\"]=\"True\"" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "3K9Ndy1beyfG", - "colab_type": "code", - "colab": {} - }, - "source": [ - "import deeplabcut" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "o4orkg9QTHKK", - "colab_type": "code", - "colab": {} - }, - "source": [ - "deeplabcut.__version__" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "Z7ZlDr3wV4D1", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#This creates a path variable that links to your google drive copy\n", - "#No need to edit this, as you set it up before: \n", - "path_config_file = '/content/drive/My Drive/'+ProjectFolderName+'/config.yaml'\n", - "path_config_file" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xNi9s1dboEJN", - "colab_type": "text" - }, - "source": [ - "## Create a training dataset:\n", - "### You must do this step inside of Colab:\n", - "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", - "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**.\n", - "\n", - "Now it is the time to start training the network!" - ] - }, - { - "cell_type": "code", - "metadata": { - "scrolled": true, - "id": "eMeUwgxPoEJP", - "colab_type": "code", - "colab": {} - }, - "source": [ - "# Note: if you are using the demo data (i.e. examples/Reaching-Mackenzie-2018-08-30/), first delete the folder called dlc-models! \n", - "#Then, run this cell. There are many more functions you can set here, including which netowkr to use!\n", - "#check the docstring for full options you can do!\n", - "deeplabcut.create_training_dataset(path_config_file, net_type='resnet_50', augmenter_type='imgaug')" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "c4FczXGDoEJU", - "colab_type": "text" - }, - "source": [ - "## Start training:\n", - "This function trains the network for a specific shuffle of the training dataset. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "_pOvDq_2oEJW", - "colab_type": "code", - "colab": {} - }, - "source": [ - "#let's also change the display and save_iters just in case Colab takes away the GPU... \n", - "#if that happens, you can reload from a saved point. Typically, you want to train to 200,000 + iterations.\n", - "#more info and there are more things you can set: https://github.com/AlexEMG/DeepLabCut/blob/master/docs/functionDetails.md#g-train-the-network\n", - "\n", - "deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10,saveiters=500)\n", - "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 1.03M iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry...." - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RiDwIVf5-3H_", - "colab_type": "text" - }, - "source": [ - "**When you hit \"STOP\" you will get a KeyInterrupt \"error\"! No worries! :)**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xZygsb2DoEJc", - "colab_type": "text" - }, - "source": [ - "## Start evaluating:\n", - "This funtion evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "nv4zlbrnoEJg", - "colab_type": "code", - "colab": {} - }, - "source": [ - "%matplotlib notebook\n", - "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 good as the labeler, \n", - "#so be sure your labels are good! (And you have trained enough ;)" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BaLBl3TQtrfB", - "colab_type": "text" - }, - "source": [ - "## There is an optional refinement step you can do outside of Colab:\n", - "- if your pixel errors are not low enough, please check out the protocol guide on how to refine your network!\n", - "- You will need to adjust the labels **outside of Colab!** We recommend coming back to train and analyze videos... \n", - "- Please see the repo and protocol instructions on how to refine your data!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OVFLSKKfoEJk", - "colab_type": "text" - }, - "source": [ - "## Start Analyzing videos: \n", - "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", - "\n", - "The results are stored in hd5 file in the same directory where the video resides. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Y_LZiS_0oEJl", - "colab_type": "code", - "colab": {} - }, - "source": [ - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype=VideoType)" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "8GTiuJESoEKH", - "colab_type": "text" - }, - "source": [ - "## Plot the trajectories of the analyzed videos:\n", - "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "gX21zZbXoEKJ", - "colab_type": "code", - "colab": {} - }, - "source": [ - "deeplabcut.plot_trajectories(path_config_file,videofile_path, videotype=VideoType)" - ], - "execution_count": 0, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pqaCw15v8EmB", - "colab_type": "text" - }, - "source": [ - "Now you can look at the plot-poses file and check the \"plot-likelihood.png\" might want to change the \"p-cutoff\" in the config.yaml file so that you have only high confidnece points plotted in the video. i.e. ~0.8 or 0.9. The current default is 0.4. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pCrUvQIvoEKD", - "colab_type": "text" - }, - "source": [ - "## Create labeled video:\n", - "This funtion is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "6aDF7Q7KoEKE", - "colab_type": "code", - "colab": {} - }, - "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path, videotype=VideoType)" - ], - "execution_count": 0, - "outputs": [] - } - ] -} \ No newline at end of file diff --git a/examples/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb deleted file mode 100644 index 89778009aa..0000000000 --- a/examples/COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb +++ /dev/null @@ -1,547 +0,0 @@ -{ - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "name": "COLAB_maDLC_TrainNetwork_VideoAnalysis.ipynb", - "provenance": [], - "collapsed_sections": [], - "toc_visible": true, - "include_colab_link": true - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "accelerator": "GPU" - }, - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "view-in-github", - "colab_type": "text" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RK255E7YoEIt" - }, - "source": [ - "# DeepLabCut 2.2 Toolbox - COLAB\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1590444465547-SHXODUII311HEE407IL6/ke17ZwdGBToddI8pDm48kE4VnnB9_j2k1VP236ADqAFZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpxQg9Vf0owGyf3dhfDKy8SxMujaKmp2B54Sb3VS1rO76Whq-cUhHVuKFlGUXsU9tJk/ezgif.com-video-to-gif.gif?format=1500w)\n", - "\n", - "https://github.com/DeepLabCut/DeepLabCut\n", - "\n", - "This notebook illustrates how to, for multi-animal projects, use the cloud-based GPU to:\n", - "- create a multi-animal training set\n", - "- train a network\n", - "- evaluate a network\n", - "- cross evaluate inference parameters\n", - "- analyze novel videos\n", - "- assemble tracklets\n", - "- create quality check plots\n", - "\n", - "###This notebook assumes you already have a project folder with labeled data! \n", - "\n", - "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", - "\n", - "This shows the most simple code to do so, but many of the functions have additional features, so please check out the docs on GitHub.\n", - "\n", - "Mathis et al, in prep. <- please note, we are providing this toolbox as an early access release; more feeatures and details will be released with the forthcoming paper.\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "txoddlM8hLKm" - }, - "source": [ - "## First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "q23BzhA6CXxu" - }, - "source": [ - "#(this will take a few minutes to install all the dependences!)\n", - "!pip install deeplabcut\n", - "%reload_ext numpy\n", - "%reload_ext scipy\n", - "%reload_ext matplotlib\n", - "%reload_ext mpl_toolkits" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "Y36K4Eux3h-X" - }, - "source": [ - "" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "code", - "metadata": { - "id": "oTwAcbq2-FZz" - }, - "source": [ - "# Use TensorFlow 1.x:\n", - "%tensorflow_version 1.x\n", - "\n", - "#GUIs don't work on the cloud, so we supress them:\n", - "import os\n", - "os.environ[\"DLClight\"]=\"True\"\n", - "import deeplabcut" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "cQ-nlTkri4HZ" - }, - "source": [ - "## Link your Google Drive (with your labeled data):\n", - "\n", - "### First, place your porject folder into you google drive! \"i.e. move the folder named \"Project-YourName-TheDate\" into google drive." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "KS4Q4UkR9rgG" - }, - "source": [ - "#Now, let's link to your GoogleDrive. Run this cell and follow the authorization instructions:\n", - "#(We recommend putting a copy of the github repo in your google drive if you are using the demo \"examples\")\n", - "\n", - "from google.colab import drive\n", - "drive.mount('/content/drive')" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Frnj1RVDyEqs" - }, - "source": [ - "## YOU WILL NEED TO EDIT THE PROJECT PATH **in the config.yaml file** TO BE SET TO YOUR GOOGLE DRIVE LINK!\n", - "\n", - "Now, you can do this within COLAB! Simply navigate in the left panel to your project folder, double click on the config.yaml file, and you will see it load on the right! Edit the first part of your path, to match:\n", - "\n", - " /content/drive/My Drive/yourProjectFolderName\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "vhENAlQnFENJ" - }, - "source": [ - "# PLEASE EDIT THIS:\n", - "ProjectFolderName = 'teamDLC-myProject-2021-01-06'\n", - "VideoType = 'mp4' #, mp4, MOV, or avi, whatever you uploaded!\n", - "\n", - "\n", - "# No need to edit this, we are going to assume you put videos you want to analyze in the \"videos\" folder, but if this is NOT true, edit below:\n", - "videofile_path = ['/content/drive/My Drive/'+ProjectFolderName+'/videos/'] #Enter the list of videos or folder to analyze.\n", - "videofile_path\n", - "\n", - "\n", - "#No need to edit this, as you set it when you passed the ProjectFolderName (above): \n", - "path_config_file = '/content/drive/My Drive/'+ProjectFolderName+'/config.yaml'\n", - "path_config_file\n", - "#This creates a path variable that links to your google drive project" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xNi9s1dboEJN" - }, - "source": [ - "## Create a multi-animal training dataset:\n", - "### You must do this step inside of Colab:\n", - "\n", - "- Reminder: you must connect EVERY bodypart in a skeleton before you run this step! You should OVER CONNECT all the parts for training. You can do this in the GUI before you upload your project (and in the future this will be automatically done).\n", - "\n", - "See docs for crucial details on how to do this effciently: https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/functionDetails.md#b-configure-the-project-\n", - "\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1589256735280-SCN7CROSJNJWCDS6EK5T/ke17ZwdGBToddI8pDm48kB08p9-rNkpPD7A3fw8YFjZZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZamWLI2zvYWH8K3-s_4yszcp2ryTI0HqTOaaUohrI8PIno0kSvzOWihTW1zp8-1-7mzYxUQjsVr2n3nmNdVcso4/bodyparts-skeleton.png?format=1000w)\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1589410182515-9SJO9MML6CNCXBAWQ6Z6/ke17ZwdGBToddI8pDm48kJ1oJoOIxBAgRD2ClXVCmKFZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpxBw7VlGKDQO2xTcc51Yv6DahHgScLwHgvMZoEtbzk_9vMJY_JknNFgVzVQ2g0FD_s/ezgif.com-video-to-gif+%2811%29.gif?format=750w)\n", - "\n", - "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", - "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**.\n", - "\n", - "Now it is the time to start training the network!" - ] - }, - { - "cell_type": "code", - "metadata": { - "scrolled": true, - "id": "eMeUwgxPoEJP" - }, - "source": [ - "# ATTENTION:\n", - "\n", - "#Note, you must run this. If your images are smaller than 400 by 400, please make these numbers smaller.\n", - "deeplabcut.cropimagesandlabels(path_config_file, size=(400, 400), userfeedback=False)\n", - "\n", - "#if you labeled on Windows, please set the windows2linux=True:\n", - "deeplabcut.create_multianimaltraining_dataset(path_config_file, windows2linux=False)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "c4FczXGDoEJU" - }, - "source": [ - "## Start training:\n", - "This function trains the network for a specific shuffle of the training dataset. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "_pOvDq_2oEJW" - }, - "source": [ - "#let's also change the display and save_iters just in case Colab takes away the GPU... \n", - "#if that happens, you can reload from a saved point. \n", - "#Typically, you want to train to 50,000 iterations.\n", - "#more info and there are more things you can set: https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/functionDetails.md#g-train-the-network\n", - "\n", - "#which shuffle do you want to train?\n", - "shuffle = 1\n", - "deeplabcut.train_network(path_config_file, shuffle=shuffle, displayiters=100,saveiters=1000)\n", - "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 50K iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry...." - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RiDwIVf5-3H_" - }, - "source": [ - "**When you hit \"STOP\" you will get a KeyInterrupt \"error\"! No worries! :)**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "xZygsb2DoEJc" - }, - "source": [ - "## Start evaluating: for maDLC, this is several steps. \n", - " - First, we evaluate the pose estimation performance, and then we can cross-valudate optimal inference parameters.\n", - "\n", - "- This funtion evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images) and stores the results as .5 and .csv file in a subdirectory under **evaluation-results**\n", - "\n", - "- If the scoremaps do not look accurate, don't proceed to tracklet assembly; please consider (1) adding more data, (2) adding more bodyparts!\n", - "\n", - "Here is an example of what you'd aim to see before proceeding:\n", - "\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1590535809087-X655WY9W1MW1MY1I7DHE/ke17ZwdGBToddI8pDm48kBoswZhKnUtAF7-bTXgw67EUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYxCRW4BPu10St3TBAUQYVKc5tTP1cnANTUwNNPnYFjIp6XbP9N1GxIgAkxvBVqt0UvLpPHYwvNQTwHg8f_Zu8ZF/evaluation.png?format=1000w)\n", - "\n" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "nv4zlbrnoEJg" - }, - "source": [ - "#let's evaluate first:\n", - "deeplabcut.evaluate_network(path_config_file,Shuffles=[shuffle], plotting=True,c_engine=False)\n", - "#plot a few scoremaps:\n", - "deeplabcut.extract_save_all_maps(path_config_file, shuffle=shuffle, Indices=[0])" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "fYlGbloolDU2" - }, - "source": [ - "IF these images, numbers, and maps do not look good, do not proceed. You should increase the diversity and number of frames you label, and re-create a training dataset and re-train! " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "4MbIL5z2U7fp" - }, - "source": [ - "NOTE: this optimized part detection for video analysis. It cannot optimze for tracking, as this is use-case dependent. Please check the docs on how you can set the best parameters and modify/test before \"final\" tracking parameters. You can use COLAB to analyze videos, but afterwards we recommend using the outputs/proejct folder locally to run the final steps! They do not require a GPU. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "3s2SjwcIk-R9" - }, - "source": [ - "#Cross-validate for Animal Assembly & Tracking:\n", - "deeplabcut.evaluate_multianimal_crossvalidate(\n", - " path_config_file,\n", - " Shuffles=[shuffle],\n", - " edgewisecondition=True,\n", - " leastbpts=1,\n", - " init_points=20,\n", - " n_iter=100,\n", - " target='rpck_train',\n", - " )" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "OVFLSKKfoEJk" - }, - "source": [ - "## Start Analyzing videos: \n", - "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", - "\n", - "The results are stored in hd5 file in the same directory where the video resides. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "Y_LZiS_0oEJl" - }, - "source": [ - "print(\"Start Analyzing my video(s)!\")\n", - "scorername = deeplabcut.analyze_videos(path_config_file, \n", - " videofile_path, \n", - " shuffle=shuffle, \n", - " videotype=VideoType, \n", - " c_engine=False)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PxRLS2_-r55K" - }, - "source": [ - "## The steps below work on a single video at a time.\n", - "- Here you can create a video to check the pose estimation detection quality! If this looks good, proceed to tracklet conversions with the interactive GUI (ouside of COLAB for now), or if you know your optimal parameters, you can automate this and run the additional steps shown in a few cells down." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "65mWwX5bTc5C" - }, - "source": [ - "##### PROTIP: #####\n", - "## look at the output video; if the pose estimation (i.e. key points)\n", - "## don't look good, don't proceed with tracking - add more data to your training set and re-train!\n", - "\n", - "#let's check a specific video (PLEASE EDIT VIDEO PATH):\n", - "Specific_videofile = '/content/drive/My Drive/yourproject/videos/demo.mp4'\n", - "\n", - "from deeplabcut.utils import auxiliaryfunctions\n", - "scorername, DLCscorerlegacy = auxiliaryfunctions.GetScorerName(path_config_file, shuffle, trainFraction=0)\n", - "print(\"scorename is: \"+scorername)\n", - "\n", - "deeplabcut.create_video_with_all_detections(path_config_file, [Specific_videofile], scorername)\n", - "\n", - "#Again, if this does not look perfect, do not proceed! Retrain with more diverse data." - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "78x3BbotIsuO" - }, - "source": [ - "## Convert Detections to Tracklets:\n", - "\n", - "- The idea is that you test and adapt hyperparameters for tracking outside of COLAB. Once you have good parameters, this can be automated on future videos. Shown here!\n", - "\n", - "- I.e., instead of always doing an interactive parameter setting step, you can simply convert tracklets to .h5 files using these parameters (see GitHub for more info)." - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "zIvXM7TXIs-U" - }, - "source": [ - "#assemble tracklets:\n", - "#read the docs: which tracker to test out (you can run this many times to try multiple):\n", - "tracktype= 'box' #box, skeleton, ellipse\n", - "\n", - "deeplabcut.convert_detections2tracklets(path_config_file, Specific_videofile, videotype=VideoType,\n", - " shuffle=shuffle, track_method=tracktype, overwrite=True)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "HHH9lCM7JCZ0" - }, - "source": [ - "## Now you should manually verify the tracks and correct them if needed! [currently only working outside of COLAB]" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "ocRzxq7fJCjm" - }, - "source": [ - "''' here is the code you would need:\n", - "os.environ[\"DLClight\"]=\"False\"\n", - "import deeplabcut\n", - "\n", - "#ATTENTION:\n", - "picklefile = '/...._10000_bx.pickle' #(see your video folder for path i.e. right click and say copy path!!!)\n", - "vid ='/yourVIDEO.mp4'\n", - "#if you want occlusions filled in, tell us how many frames to fill in, i.e. if there is a gap in data:\n", - "framestofill = 0. #note, put \"0\" if you want ALL gaps filled!\n", - "\n", - "%matplotlib inline\n", - "\n", - "from deeplabcut import refine_tracklets\n", - "TrackletManager, TrackletVisualizer = refine_tracklets(path_config_file, \n", - " picklefile, \n", - " Specific_videofile, \n", - " min_swap_frac=0,\n", - " min_tracklet_frac=0, \n", - " max_gap=framestofill)\n", - "'''" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "AUOKrLJRseUX" - }, - "source": [ - "## Let's assume you have great tracking parameters, and you want to analyze a full set of videos:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "a6izVWX8sdzL" - }, - "source": [ - "#^^^^^^^^^You do NOT neeed to run if you hit \"save\" in the GUI ^^^^^^^^^^\n", - "#this is just if you want to run the same parameters over a set of videos!\n", - "\n", - "# You need to point to your pickle file, please \"copy path\" from the folder to the left (right click, copy path)\n", - "picklefile = '/content/drive/My Drive/mwm-penguins-2020-03-31/videos/penguindemoDLC_resnet50_mwmMar31shuffle1_22000_bx.pickle' #(see your video folder for path i.e. right click and say copy path!!!)\n", - "vid ='/content/drive/My Drive/mwm-penguins-2020-03-31/videos/penguindemo.mp4'\n", - "\n", - "deeplabcut.convert_raw_tracks_to_h5(path_config_file, picklefile)\n", - "deeplabcut.filterpredictions(path_config_file, \n", - " videofile_path, \n", - " videotype=VideoType, \n", - " track_method = tracktype)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "Zk4xGb8Ftf3B" - }, - "source": [ - "## Create plots of your trajectories:" - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "gX21zZbXoEKJ" - }, - "source": [ - "deeplabcut.plot_trajectories(path_config_file,videofile_path, videotype=VideoType, track_method=tracktype)" - ], - "execution_count": null, - "outputs": [] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pqaCw15v8EmB" - }, - "source": [ - "Now you can look at the plot-poses file and check the \"plot-likelihood.png\" might want to change the \"p-cutoff\" in the config.yaml file so that you have only high confidnece points plotted in the video. i.e. ~0.8 or 0.9. The current default is 0.4. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "pCrUvQIvoEKD" - }, - "source": [ - "## Create labeled video:\n", - "This funtion is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " - ] - }, - { - "cell_type": "code", - "metadata": { - "id": "6aDF7Q7KoEKE" - }, - "source": [ - "deeplabcut.create_labeled_video(path_config_file,\n", - " videofile_path, \n", - " shuffle=shuffle, \n", - " draw_skeleton=True, \n", - " videotype=VideoType, \n", - " save_frames=False,\n", - " filtered=True, \n", - " track_method = tracktype)" - ], - "execution_count": null, - "outputs": [] - } - ] -} diff --git a/examples/Demo_labeledexample_Openfield.ipynb b/examples/Demo_labeledexample_Openfield.ipynb deleted file mode 100644 index 0d4f69abc5..0000000000 --- a/examples/Demo_labeledexample_Openfield.ipynb +++ /dev/null @@ -1,668 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "N0gDJMy1ywm8" - }, - "source": [ - "# DeepLabCut Toolbox - Open-Field DEMO\n", - "https://github.com/AlexEMG/DeepLabCut\n", - "\n", - "#### The notebook accompanies the following user-guide:\n", - "\n", - "Nath\\*, Mathis\\* et al. *Using DeepLabCut for markerless pose estimation during behavior across species* Nature Protocols, 2019: https://www.nature.com/articles/s41596-019-0176-0\n", - "\n", - "This notebook illustrates how to:\n", - "- load the demo project\n", - "- train a network\n", - "- evaluate a network\n", - "- analyze a novel video\n", - "- create an automatically labeled video \n", - "- plot the trajectories \n", - "- identify outlier frames\n", - "- annotate the outlier frames manually\n", - "- merge the data sets and update the training set\n", - "- train a network\n", - "\n", - "Note: This notebook starts from an already initialized project with labeled data.\n", - "\n", - "\n", - "The data is a subset from *DeepLabCut: markerless pose estimation of user-defined body parts with deep learning* https://www.nature.com/articles/s41593-018-0209-y (this subset was not used to train models that are shown or evaluated in our paper)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "hTtJxcQ7ywnB" - }, - "outputs": [], - "source": [ - "# Importing the toolbox (takes several seconds)\n", - "import deeplabcut" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "WOEHc0MeywnJ" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loaded, now creating training data...\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30 already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/short_mp3/CollectedData_Pranav.h5 not found (perhaps not annotated)\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1 already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1//train already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1//test already exists!\n", - "The training dataset is successfully created. Use the function 'train_network' to start training. Happy training!\n" - ] - } - ], - "source": [ - "# Loading example data set:\n", - "import os\n", - "# Note that parameters of this project can be seen at: *openfield-Pranav-2018-10-30/config.yaml*\n", - "from pathlib import Path\n", - "path_config_file = os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/config.yaml')\n", - "deeplabcut.load_demo_data(path_config_file)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "ROlflqQLywnP" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating images with labels by Pranav.\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/m4s1_labeled already exists!\n", - "They are stored in the following folder: /home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/m4s1_labeled.\n", - "Attention: /home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/labeled-data/short_mp3 does not appear to have labeled data!\n", - "If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!\n" - ] - } - ], - "source": [ - "#[OPTIONAL] Perhaps plot the labels to see how the frames were annotated:\n", - "#(note, this project was created in Linux, so you might have an error in Windows, but this is an optional step)\n", - "deeplabcut.check_labels(path_config_file)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "h9H7eqDLywnV" - }, - "source": [ - "## Start training of Feature Detectors\n", - "This function trains the network for a specific shuffle of the training dataset. The user can set various parameters in */openfield-Pranav-2018-10-30/dlc-models/.../pose_cfg.yaml*. \n", - "\n", - "Training can be stopped at any time. Note that the weights are only stored every 'save_iters' steps. For this demo the state it is advisable to store & display the progress very often. In practice this is inefficient. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "jg96O2acywnW", - "scrolled": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Config:\n", - "{'all_joints': [[0], [1], [2], [3]],\n", - " 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'],\n", - " 'batch_size': 1,\n", - " 'bottomheight': 400,\n", - " 'crop': True,\n", - " 'crop_pad': 0,\n", - " 'cropratio': 0.4,\n", - " 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat',\n", - " 'dataset_type': 'default',\n", - " 'display_iters': 1000,\n", - " 'fg_fraction': 0.25,\n", - " 'global_scale': 0.8,\n", - " 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt',\n", - " 'intermediate_supervision': False,\n", - " 'intermediate_supervision_layer': 12,\n", - " 'leftwidth': 400,\n", - " 'location_refinement': True,\n", - " 'locref_huber_loss': True,\n", - " 'locref_loss_weight': 0.05,\n", - " 'locref_stdev': 7.2801,\n", - " 'log_dir': 'log',\n", - " 'max_input_size': 1500,\n", - " 'mean_pixel': [123.68, 116.779, 103.939],\n", - " 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle',\n", - " 'min_input_size': 64,\n", - " 'minsize': 100,\n", - " 'mirror': False,\n", - " 'multi_step': [[0.005, 10000],\n", - " [0.02, 430000],\n", - " [0.002, 730000],\n", - " [0.001, 1030000]],\n", - " 'net_type': 'resnet_50',\n", - " 'num_joints': 4,\n", - " 'optimizer': 'sgd',\n", - " 'pos_dist_thresh': 17,\n", - " 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30',\n", - " 'regularize': False,\n", - " 'rightwidth': 400,\n", - " 'save_iters': 50000,\n", - " 'scale_jitter_lo': 0.5,\n", - " 'scale_jitter_up': 1.25,\n", - " 'scoremap_dir': 'test',\n", - " 'shuffle': True,\n", - " 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/snapshot',\n", - " 'stride': 8.0,\n", - " 'topheight': 400,\n", - " 'use_gt_segm': False,\n", - " 'video': False,\n", - " 'video_batch': False,\n", - " 'weigh_negatives': False,\n", - " 'weigh_only_present_joints': False,\n", - " 'weigh_part_predictions': False,\n", - " 'weight_decay': 0.0001}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO:tensorflow:Restoring parameters from /home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt\n", - "Display_iters overwritten as 10\n", - "Save_iters overwritten as 100\n", - "Training parameter:\n", - "{'stride': 8.0, 'weigh_part_predictions': False, 'weigh_negatives': False, 'fg_fraction': 0.25, 'weigh_only_present_joints': False, 'mean_pixel': [123.68, 116.779, 103.939], 'shuffle': True, 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/snapshot', 'log_dir': 'log', 'global_scale': 0.8, 'location_refinement': True, 'locref_stdev': 7.2801, 'locref_loss_weight': 0.05, 'locref_huber_loss': True, 'optimizer': 'sgd', 'intermediate_supervision': False, 'intermediate_supervision_layer': 12, 'regularize': False, 'weight_decay': 0.0001, 'mirror': False, 'crop_pad': 0, 'scoremap_dir': 'test', 'dataset_type': 'default', 'use_gt_segm': False, 'batch_size': 1, 'video': False, 'video_batch': False, 'crop': True, 'cropratio': 0.4, 'minsize': 100, 'leftwidth': 400, 'rightwidth': 400, 'topheight': 400, 'bottomheight': 400, 'all_joints': [[0], [1], [2], [3]], 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'], 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat', 'display_iters': 1000, 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt', 'max_input_size': 1500, 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle', 'min_input_size': 64, 'multi_step': [[0.005, 10000], [0.02, 430000], [0.002, 730000], [0.001, 1030000]], 'net_type': 'resnet_50', 'num_joints': 4, 'pos_dist_thresh': 17, 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30', 'save_iters': 50000, 'scale_jitter_lo': 0.5, 'scale_jitter_up': 1.25}\n", - "Starting training....\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "iteration: 10 loss: 0.3308 lr: 0.005\n", - "iteration: 20 loss: 0.0563 lr: 0.005\n", - "iteration: 30 loss: 0.0417 lr: 0.005\n", - "iteration: 40 loss: 0.0362 lr: 0.005\n", - "iteration: 50 loss: 0.0407 lr: 0.005\n", - "iteration: 60 loss: 0.0461 lr: 0.005\n", - "iteration: 70 loss: 0.0385 lr: 0.005\n", - "iteration: 80 loss: 0.0345 lr: 0.005\n", - "iteration: 90 loss: 0.0314 lr: 0.005\n", - "iteration: 100 loss: 0.0428 lr: 0.005\n", - "iteration: 110 loss: 0.0262 lr: 0.005\n", - "iteration: 120 loss: 0.0255 lr: 0.005\n", - "iteration: 130 loss: 0.0275 lr: 0.005\n", - "iteration: 140 loss: 0.0251 lr: 0.005\n", - "iteration: 150 loss: 0.0221 lr: 0.005\n", - "iteration: 160 loss: 0.0209 lr: 0.005\n", - "iteration: 170 loss: 0.0297 lr: 0.005\n", - "iteration: 180 loss: 0.0325 lr: 0.005\n", - "iteration: 190 loss: 0.0242 lr: 0.005\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mdeeplabcut\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_network\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath_config_file\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshuffle\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdisplayiters\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msaveiters\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m100\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/training.py\u001b[0m in \u001b[0;36mtrain_network\u001b[0;34m(config, shuffle, trainingsetindex, gputouse, max_snapshots_to_keep, autotune, displayiters, saveiters, maxiters)\u001b[0m\n\u001b[1;32m 87\u001b[0m \u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mposeconfigfile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mdisplayiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0msaveiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmaxiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmax_to_keep\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_snapshots_to_keep\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m#pass on path and file name for pose_cfg.yaml!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 88\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 89\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 90\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 91\u001b[0m \u001b[0mos\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mchdir\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstart_path\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/training.py\u001b[0m in \u001b[0;36mtrain_network\u001b[0;34m(config, shuffle, trainingsetindex, gputouse, max_snapshots_to_keep, autotune, displayiters, saveiters, maxiters)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 86\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 87\u001b[0;31m \u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mposeconfigfile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mdisplayiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0msaveiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmaxiters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mmax_to_keep\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_snapshots_to_keep\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m#pass on path and file name for pose_cfg.yaml!\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 88\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 89\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/train.py\u001b[0m in \u001b[0;36mtrain\u001b[0;34m(config_yaml, displayiters, saveiters, maxiters, max_to_keep)\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[0mcurrent_lr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlr_gen\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_lr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 141\u001b[0m [_, loss_val, summary] = sess.run([train_op, total_loss, merged_summaries],\n\u001b[0;32m--> 142\u001b[0;31m feed_dict={learning_rate: current_lr})\n\u001b[0m\u001b[1;32m 143\u001b[0m \u001b[0mcum_loss\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mloss_val\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 144\u001b[0m \u001b[0mtrain_writer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0madd_summary\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msummary\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self, fetches, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 898\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 899\u001b[0m result = self._run(None, fetches, feed_dict, options_ptr,\n\u001b[0;32m--> 900\u001b[0;31m run_metadata_ptr)\n\u001b[0m\u001b[1;32m 901\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrun_metadata\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 902\u001b[0m \u001b[0mproto_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtf_session\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTF_GetBuffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrun_metadata_ptr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_run\u001b[0;34m(self, handle, fetches, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 1133\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfinal_fetches\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mfinal_targets\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mhandle\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mfeed_dict_tensor\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1134\u001b[0m results = self._do_run(handle, final_targets, final_fetches,\n\u001b[0;32m-> 1135\u001b[0;31m feed_dict_tensor, options, run_metadata)\n\u001b[0m\u001b[1;32m 1136\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1137\u001b[0m \u001b[0mresults\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_do_run\u001b[0;34m(self, handle, target_list, fetch_list, feed_dict, options, run_metadata)\u001b[0m\n\u001b[1;32m 1314\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mhandle\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1315\u001b[0m return self._do_call(_run_fn, feeds, fetches, targets, options,\n\u001b[0;32m-> 1316\u001b[0;31m run_metadata)\n\u001b[0m\u001b[1;32m 1317\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1318\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_do_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_prun_fn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhandle\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeeds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetches\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_do_call\u001b[0;34m(self, fn, *args)\u001b[0m\n\u001b[1;32m 1320\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_do_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1321\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1322\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1323\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0merrors\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOpError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1324\u001b[0m \u001b[0mmessage\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcompat\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mas_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmessage\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_run_fn\u001b[0;34m(feed_dict, fetch_list, target_list, options, run_metadata)\u001b[0m\n\u001b[1;32m 1305\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_extend_graph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1306\u001b[0m return self._call_tf_sessionrun(\n\u001b[0;32m-> 1307\u001b[0;31m options, feed_dict, fetch_list, target_list, run_metadata)\n\u001b[0m\u001b[1;32m 1308\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1309\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_prun_fn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhandle\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeed_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetch_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/tensorflow/python/client/session.py\u001b[0m in \u001b[0;36m_call_tf_sessionrun\u001b[0;34m(self, options, feed_dict, fetch_list, target_list, run_metadata)\u001b[0m\n\u001b[1;32m 1407\u001b[0m return tf_session.TF_SessionRun_wrapper(\n\u001b[1;32m 1408\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_session\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moptions\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfeed_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfetch_list\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtarget_list\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1409\u001b[0;31m run_metadata)\n\u001b[0m\u001b[1;32m 1410\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1411\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0merrors\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraise_exception_on_not_ok_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mstatus\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10, saveiters=100)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Note, that if it reaches the end or you stop it (by hitting \"stop\" or by CTRL+C), \n", - "you will see an \"KeyboardInterrupt\" error, but you can ignore this!**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "CCzxVT_gywnc" - }, - "source": [ - "## Evaluate a trained network\n", - "\n", - "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results**.\n", - "\n", - "You can change various parameters in the ```config.yaml``` file of this project. For evaluation all the model descriptors (Task, TrainingFraction, Date etc.) are important. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated postions need to be so that they are shown in the plots. One can furthermore, change the colormap and dotsize for those graphs." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "kuprPKDdywne", - "scrolled": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Config:\n", - "{'all_joints': [[0], [1], [2], [3]],\n", - " 'all_joints_names': ['snout', 'leftear', 'rightear', 'tailbase'],\n", - " 'batch_size': 1,\n", - " 'bottomheight': 400,\n", - " 'crop': True,\n", - " 'crop_pad': 0,\n", - " 'cropratio': 0.4,\n", - " 'dataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.mat',\n", - " 'dataset_type': 'default',\n", - " 'display_iters': 1000,\n", - " 'fg_fraction': 0.25,\n", - " 'global_scale': 0.8,\n", - " 'init_weights': '/home/mackenzie/anaconda3/envs/DLC2/lib/python3.6/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt',\n", - " 'intermediate_supervision': False,\n", - " 'intermediate_supervision_layer': 12,\n", - " 'leftwidth': 400,\n", - " 'location_refinement': True,\n", - " 'locref_huber_loss': True,\n", - " 'locref_loss_weight': 0.05,\n", - " 'locref_stdev': 7.2801,\n", - " 'log_dir': 'log',\n", - " 'max_input_size': 1500,\n", - " 'mean_pixel': [123.68, 116.779, 103.939],\n", - " 'metadataset': 'training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/Documentation_data-openfield_95shuffle1.pickle',\n", - " 'min_input_size': 64,\n", - " 'minsize': 100,\n", - " 'mirror': False,\n", - " 'multi_step': [[0.005, 10000],\n", - " [0.02, 430000],\n", - " [0.002, 730000],\n", - " [0.001, 1030000]],\n", - " 'net_type': 'resnet_50',\n", - " 'num_joints': 4,\n", - " 'optimizer': 'sgd',\n", - " 'pos_dist_thresh': 17,\n", - " 'project_path': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30',\n", - " 'regularize': False,\n", - " 'rightwidth': 400,\n", - " 'save_iters': 50000,\n", - " 'scale_jitter_lo': 0.5,\n", - " 'scale_jitter_up': 1.25,\n", - " 'scoremap_dir': 'test',\n", - " 'shuffle': True,\n", - " 'snapshot_prefix': '/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/test/snapshot',\n", - " 'stride': 8.0,\n", - " 'topheight': 400,\n", - " 'use_gt_segm': False,\n", - " 'video': False,\n", - " 'video_batch': False,\n", - " 'weigh_negatives': False,\n", - " 'weigh_only_present_joints': False,\n", - " 'weigh_part_predictions': False,\n", - " 'weight_decay': 0.0001}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/evaluation-results/ already exists!\n", - "/home/mackenzie/DEEPLABCUT/3D/DeepLabCut2.0-master/examples/openfield-Pranav-2018-10-30/evaluation-results/iteration-0/openfieldOct30-trainset95shuffle1 already exists!\n", - "Running DeepCut_resnet50_openfieldOct30shuffle1_2400 with # of trainingiterations: 2400\n", - "This net has already been evaluated!\n", - "The network is evaluated and the results are stored in the subdirectory 'evaluation_results'.\n", - "If it generalizes well, choose the best model for prediction and update the config file with the appropriate index for the 'snapshotindex'.\n", - "Use the function 'analyze_video' to make predictions on new videos.\n", - "Otherwise consider retraining the network (see DeepLabCut workflow Fig 2)\n" - ] - } - ], - "source": [ - "deeplabcut.evaluate_network(path_config_file,plotting=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*NOTE: depending on your set up sometimes you get some \"matplotlib errors, but these are not important*\n", - "\n", - "Now you can go check out the images. Given the limted data input and it took ~20 mins to test this out, it is not meant to track well, so don't be alarmed. This is just to get you familiar with the workflow... " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "XeqYWGaXywnj" - }, - "source": [ - "## Analyzing videos\n", - "This function extracts the pose based on a trained network from videos. The user can choose the trained network - by default the most recent snapshot is used to analyse the videos. However, the user can also specify the snapshot index for the variable **snapshotindex** in the **config.yaml** file).\n", - "\n", - "The results are stored in hd5 file in the same directory, where the video resides. The pose array (pose vs. frame index) can also be exported as csv file (set flag to...). " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Vv9iHHLlywnl" - }, - "outputs": [], - "source": [ - "# Creating video path:\n", - "import os\n", - "videofile_path = os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "CFbPPD4hywnq", - "scrolled": false - }, - "outputs": [], - "source": [ - "print(\"Start analyzing the video!\")\n", - "#our demo video on a CPU with take ~30 min to analze! GPU is much faster!\n", - "deeplabcut.analyze_videos(path_config_file,[videofile_path])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "QQ3T3oykywnw" - }, - "source": [ - "## Create labeled video\n", - "\n", - "This function is for the visualization purpose and can be used to create a video in .mp4 format with the predicted labels. This video is saved in the same directory, where the (unlabeled) video resides. \n", - "\n", - "Various parameters can be set with regard to the colormap and the dotsize. The parameters of the " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "GhI9KLs4ywn0", - "scrolled": true - }, - "outputs": [], - "source": [ - "deeplabcut.create_labeled_video(path_config_file,[videofile_path])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "IErvm1K5ywn5" - }, - "source": [ - "## Plot the trajectories of the analyzed videos\n", - "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color. The underlying functions can easily be customized." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "mP2useJgywn7", - "scrolled": false - }, - "outputs": [], - "source": [ - "%matplotlib notebook\n", - "deeplabcut.plot_trajectories(path_config_file,[videofile_path],showfigures=True)\n", - "\n", - "#These plots can are interactive and can be customized (see https://matplotlib.org/)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "a5nOaWzXywoB" - }, - "source": [ - "## Extract outlier frames, where the predictions are off.\n", - "\n", - "This is optional step allows to add more training data when the evaluation results are poor. In such a case, the user can use the following function to extract frames where the labels are incorrectly predicted. Make sure to provide the correct value of the \"iterations\" as it will be used to create the unique directory where the extracted frames will be saved." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "RJGiDKuUywoC", - "scrolled": true - }, - "outputs": [], - "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,[videofile_path])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "jHjpscPcywoG" - }, - "source": [ - "The user can run this iteratively, and (even) extract additional frames from the same video." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "uaNUm3NSywoH" - }, - "source": [ - "## Manually correct labels\n", - "\n", - "This step allows the user to correct the labels in the extracted frames. Navigate to the folder corresponding to the video 'm3v1mp4' and use the GUI as described in the protocol to update the labels." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "OJDvJMcrywoI" - }, - "outputs": [], - "source": [ - "%gui wx\n", - "deeplabcut.refine_labels(path_config_file)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Y7efellnywoT" - }, - "outputs": [], - "source": [ - "#Perhaps plot the labels to see how how all the frames are annoted (including the refined ones)\n", - "deeplabcut.check_labels(path_config_file)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "mcuqoeRbywoL" - }, - "outputs": [], - "source": [ - "# Now merge datasets (once you refined all frames)\n", - "deeplabcut.merge_datasets(path_config_file)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "WRB9KgGsywoP" - }, - "source": [ - "## Create a new iteration of training dataset, check it and train...\n", - "\n", - "Following the refine labels, append these frames to the original dataset to create a new iteration of training dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "NGHghXfdywoQ" - }, - "outputs": [], - "source": [ - "deeplabcut.create_training_dataset(path_config_file)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "8fhL6nG2ywoW" - }, - "source": [ - "Now one can train the network again... (with the expanded data set)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "cAUxg5sgywoX" - }, - "outputs": [], - "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1)" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "name": "Demo-labeledexample-MouseReaching.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "Python [conda env:DLC2]", - "language": "python", - "name": "conda-env-DLC2-py" - }, - "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.6.9" - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/examples/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb similarity index 90% rename from examples/Demo_3D_DeepLabCut.ipynb rename to examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index fd2138a512..578a69fe01 100644 --- a/examples/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ "# 3D DeepLabCut Toolbox\n", - "https://github.com/AlexEMG/DeepLabCut\n", + "https://github.com/DeepLabCut/DeepLabCut\n", "\n", "This notebook will highlight the functionality of the newly released 3D project option (as of **2.0.7+**).\n", "\n", @@ -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)" ] }, { @@ -110,7 +110,7 @@ "- You must save the image pairs as **.jpg** files. \n", "- They should be named with the camera-# as the prefix, i.e. **camera-1-01.jpg** and **camera-2-01.jpg** for the first pair of images. \n", "- While taking the images:\n", - " - Keep the orientation of the chessboard same and do not rotate more than 30 degrees. Rotating the chessboard circularly will change the origin across the frames and may result in incorrect order of detected corners.\n", + " - Keep the orientation of the chessboard same and do not rotate more than 30 degrees. Rotating the chessboard circular will change the origin across the frames and may result in incorrect order of detected corners.\n", "\n", " - Cover several distances, and within each distance, cover all parts of the image view (all corners and center).\n", "\n", @@ -120,7 +120,7 @@ " \n", "#### DEMO images:\n", " \n", - "Here, we used a standard set along with this notebook. These images are a part of the Camera Calibration ToolBox for Matlab; specifically example 5: http://www.vision.caltech.edu/bouguetj/calib_doc/htmls/example5.html \n", + "Here, we used a standard set along with this notebook. These images are a part of the Camera Calibration ToolBox for Matlab; specifically example 5. The images can be downloaded at: https://data.caltech.edu/records/20164. After downloading, the calibration images can be found at ../calib_doc/htmls/calib_example.zip\n", "\n", "\n", "If you wish to run this DEMO notebook, download the files and place inside the **calibration_images** directory. (To note, pairs 1 and 6 are not detected correctly, so please delete these images!). \n", @@ -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)" ] }, { @@ -197,10 +197,9 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib\n", "%matplotlib inline\n", "\n", - "deeplabcut.check_undistortion(config_path3d)" + "deeplabcut.check_undistortion(config_path)" ] }, { @@ -239,12 +238,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_path, video_path, videotype=\"mp4\")" ] }, { @@ -269,15 +268,20 @@ "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)" ] } ], "metadata": { + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -289,7 +293,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/examples/Demo_labeledexample_MouseReaching.ipynb b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb similarity index 66% rename from examples/Demo_labeledexample_MouseReaching.ipynb rename to examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb index 425dc0f7d5..549ce4260a 100644 --- a/examples/Demo_labeledexample_MouseReaching.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb @@ -8,7 +8,11 @@ }, "source": [ "# DeepLabCut Toolbox - DEMO (mouse reaching)\n", - "https://github.com/AlexEMG/DeepLabCut\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", "\n", "#### The notebook accompanies the following user-guide:\n", "\n", @@ -35,7 +39,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Import the toolbox:" + "## Import the Toolbox and Required Libraries" ] }, { @@ -48,6 +52,8 @@ }, "outputs": [], "source": [ + "from pathlib import Path\n", + "\n", "import deeplabcut" ] }, @@ -64,12 +70,17 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "# Note that parameters of this project can be seen at: *Reaching-Mackenzie-2018-08-30/config.yaml*\n", - "from pathlib import Path\n", + "# Create a variable to set the config.yaml file path:\n", + "# 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", + "# Example - Linux/OSX\n", + "# path_config_file = \"/Users/john/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"\n", + "# Example - Windows\n", + "# path_config_file = r\"C:\\DeepLabCut\\examples\\Reaching-Mackenzie-2018-08-30\\config.yaml\"\n", "\n", - "#create a variable to set the config.yaml file path:\n", - "path_config_file = os.path.join(os.getcwd(),'Reaching-Mackenzie-2018-08-30/config.yaml')\n", + "path_config_file = str(Path.cwd() / \"Reaching-Mackenzie-2018-08-30\" / \"config.yaml\")\n", "print(path_config_file)" ] }, @@ -99,8 +110,8 @@ }, "outputs": [], "source": [ - "#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", + "# 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)" ] @@ -115,7 +126,7 @@ }, "outputs": [], "source": [ - "#Perhaps plot the labels to see how the frames were annotated:\n", + "# Perhaps plot the labels to see how the frames were annotated:\n", "\n", "deeplabcut.check_labels(path_config_file)" ] @@ -128,11 +139,12 @@ }, "source": [ "## Start training of Feature Detectors\n", - "This function trains the network for a specific shuffle of the training dataset. **The user can set various parameters in /Reaching-Mackenzie-2018-08-30/dlc-models/ReachingAug30-trainset95shuffle1/iteration-0/train/pose_cfg.yaml.**\n", "\n", - "Training can be stopped at any time. Note that the weights are only stored every 'save_iters' steps. For this demo the it is advisable to store & display the progress very often (i.e. display every 20, save every 100). In practice this is inefficient (in reality, you will train until ~200K, so we save every 50K).\n", + "This function trains the network for a specific shuffle of the training dataset. **The user can set various parameters in `.../Reaching-Mackenzie-2018-08-30/dlc-models-pytorch/iteration-0/ReachingAug30-trainset95shuffle1/train/pytorch_config.yaml`**. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", "\n", - "**We recommend just training for 10-20 min, as you aren't running this demo to use DLC, just to work through the steps. In total, this demo should take you LESS THAN 1 HOUR!**" + "Training can be stopped at any time. Note that the weights are only stored every 'save_epochs' steps. For this demo the it is advisable to store & display the progress very often (i.e. display every 20, save every 2). In practice this is inefficient (in reality, you will train until ~200, so we save every 10).\n", + "\n", + "**We recommend just training for 15-20 min, as you aren't running this demo to use DLC, just to work through the steps. In total, this demo should take you LESS THAN 1 HOUR!**" ] }, { @@ -142,24 +154,26 @@ "colab": {}, "colab_type": "code", "id": "jg96O2acywnW", - "scrolled": false + "scrolled": true }, "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file, shuffle=1, saveiters=300, displayiters=10)\n", - "#notice the variables \"saveiters\" and \"dsiplayiters\" that can be set in the function\n", + "# 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", + "# (so in this case you could stop after 2 epochs!) How do I stop? Click the STOP button!\n", "\n", - "#you just need to run this until you get at least 1 snapshot, which is set by: \"save_iters\" \n", - "#(so in this case you could stop after 500!) How do I stop? Click the STOP button!\n", - "# To train until ~2,000 iterations on a CPU should be ~30 min" + "# To train until ~50 epochs on a CPU should be ~15 min\n", + "# Every 10 epochs, your model will be evaluated. You can keep an eye on model performance\n", + "# while the model is being trained." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "*Note, that if it reaches the end (defualt 1M) or you stop it (by \"stop\" or by CTRL+C), \n", - "you will see an keyboard interrupt \"error\", but it is not a real error, i.e. you can ignore this.*" + "*Note, that if you stop it (by \"stop\" or by CTRL+C), you will see an keyboard interrupt \"error\", but it is not a real error, i.e. you can ignore this.*" ] }, { @@ -171,9 +185,9 @@ "source": [ "## Evaluate the trained network\n", "\n", - "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results**.\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**.\n", "\n", - "You can change various parameters in the ```config.yaml``` file of this project. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated postions need to be so that they are shown in the plots." + "You can change various parameters in the ```config.yaml``` file of this project. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated positions need to be so that they are shown in the plots." ] }, { @@ -187,16 +201,16 @@ }, "outputs": [], "source": [ - "deeplabcut.evaluate_network(path_config_file,plotting=True)" + "deeplabcut.evaluate_network(path_config_file, plotting=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**NOTE: depending on your set up sometimes you get some \"matplotlib errors, but these are not important**\n", + "**NOTE: depending on your setup sometimes you get some \"matplotlib errors, but these are not important**\n", "\n", - "Now you can go check out the images. Given the limted data input and it took ~20 mins to test this out, it is not meant to track well, so don't be alarmed. This is just to get you familiar with the workflow... " + "Now you can go check out the images. Given the limited data input and it took ~20 mins to test this out, it is not meant to track well, so don't be alarmed. This is just to get you familiar with the workflow... " ] }, { @@ -223,12 +237,12 @@ "outputs": [], "source": [ "# Set the video path:\n", - "#The video can be the one you trained with and new videos that look similar, i.e. same experiments, etc.\n", + "# The video can be the one you trained with and new videos that look similar, i.e. same experiments, etc.\n", "# You can add individual videos, OR just a folder - it will skip videos that are already analyzed once.\n", "\n", - "#i.e you can run 'reachingvideo1' and/or 'MovieS2_Perturbation_noLaser_compressed'\n", + "# i.e. you can run 'reachingvideo1' and/or 'MovieS2_Perturbation_noLaser_compressed'\n", "\n", - "videofile_path = os.path.join(os.getcwd(),'Reaching-Mackenzie-2018-08-30/videos/reachingvideo1.avi') " + "videofile_path = str(Path(path_config_file).parent / \"videos\" / \"reachingvideo1.avi\")" ] }, { @@ -243,8 +257,9 @@ "outputs": [], "source": [ "print(\"Start Analyzing the video!\")\n", - "deeplabcut.analyze_videos(path_config_file,[videofile_path])\n", - "# this video takes ~ 8 min to analyze with a CPU" + "\n", + "deeplabcut.analyze_videos(path_config_file, [videofile_path])\n", + "# this video takes ~ 1 min to analyze with a CPU" ] }, { @@ -279,7 +294,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,[videofile_path], draw_skeleton=True)" + "deeplabcut.create_labeled_video(path_config_file, [videofile_path], draw_skeleton=True)" ] }, { @@ -305,9 +320,9 @@ "outputs": [], "source": [ "%matplotlib notebook\n", - "deeplabcut.plot_trajectories(path_config_file,[videofile_path],showfigures=True)\n", + "deeplabcut.plot_trajectories(path_config_file, [videofile_path], showfigures=True)\n", "\n", - "#These plots can are interactive and can be customized (see https://matplotlib.org/)" + "# These plots are interactive and can be customized (see https://matplotlib.org/)" ] }, { @@ -339,11 +354,16 @@ "colab": {}, "colab_type": "code", "id": "RJGiDKuUywoC", - "scrolled": false + "scrolled": true }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,videofile_path,outlieralgorithm='uncertain',p_bound=.2)" + "deeplabcut.extract_outlier_frames(\n", + " path_config_file,\n", + " videofile_path,\n", + " outlieralgorithm=\"uncertain\",\n", + " p_bound=0.2,\n", + ")" ] }, { @@ -365,7 +385,9 @@ "source": [ "## Manually correct labels\n", "\n", - "This step allows the user to correct the labels in the extracted frames. Navigate to the folder with the videos and use the GUI as described in the protocol to update the labels." + "This step allows the user to correct the labels in the extracted frames. Navigate to the folder with the videos and use the GUI as described in the protocol to update the labels.\n", + "\n", + "For documentation regarding the GUI, [look at the docs for `napari-deeplabcut`](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) - and specifically _\"3. Refining labels – the image folder contains a machinelabels-iter<#>.h5 file.\"_!" ] }, { @@ -379,9 +401,6 @@ }, "outputs": [], "source": [ - "#GUI pops up! \n", - "#sometimes you need to restart the kernel for the GUI to launch.\n", - "%gui wx\n", "deeplabcut.refine_labels(path_config_file)" ] }, @@ -421,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!" ] @@ -436,7 +455,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_training_dataset(path_config_file)" + "deeplabcut.create_training_dataset(path_config_file, engine=deeplabcut.Engine.PYTORCH)" ] }, { @@ -446,7 +465,7 @@ "id": "8fhL6nG2ywoW" }, "source": [ - "Now one can train the network again... (with the expanded data set)" + "Now one can train the network again... (with the expanded data set). We can continue training from the snapshot we already have by using the `snapshot_path` argument - instead of training the model from scratch, it will load the weights we already have and fine-tune them!" ] }, { @@ -459,8 +478,31 @@ }, "outputs": [], "source": [ - "deeplabcut.train_network(path_config_file)" + "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", + ")\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=10,\n", + " batch_size=8,\n", + " snapshot_path=snapshot_path,\n", + ")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -470,10 +512,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -485,7 +532,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb new file mode 100644 index 0000000000..0d26f986fe --- /dev/null +++ b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "N0gDJMy1ywm8" + }, + "source": [ + "# DeepLabCut Toolbox - Open-Field DEMO\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", + "\n", + "#### The notebook accompanies the following user-guide:\n", + "\n", + "Nath\\*, Mathis\\* et al. *Using DeepLabCut for markerless pose estimation during behavior across species* Nature Protocols, 2019: https://www.nature.com/articles/s41596-019-0176-0\n", + "\n", + "This notebook illustrates how to:\n", + "- load the demo project\n", + "- train a network\n", + "- evaluate a network\n", + "- analyze a novel video\n", + "- create an automatically labeled video \n", + "- plot the trajectories \n", + "- identify outlier frames\n", + "- annotate the outlier frames manually\n", + "- merge the data sets and update the training set\n", + "- train a network\n", + "\n", + "Note: This notebook starts from an already initialized project with labeled data.\n", + "\n", + "\n", + "The data is a subset from *DeepLabCut: markerless pose estimation of user-defined body parts with deep learning* https://www.nature.com/articles/s41593-018-0209-y (this subset was not used to train models that are shown or evaluated in our paper)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "hTtJxcQ7ywnB" + }, + "outputs": [], + "source": [ + "# Importing the toolbox (takes several seconds)\n", + "from pathlib import Path\n", + "\n", + "import deeplabcut" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "WOEHc0MeywnJ" + }, + "outputs": [], + "source": [ + "# Create a variable to set the config.yaml file path:\n", + "# 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", + "# Example - Linux/OSX\n", + "# path_config_file = \"/Users/john/DeepLabCut/examples/openfield-Pranav-2018-10-30/config.yaml\"\n", + "# Example - Windows\n", + "# path_config_file = r\"C:\\DeepLabCut\\examples\\openfield-Pranav-2018-10-30\\config.yaml\"\n", + "#\n", + "# Note that parameters of this project can be seen at: *openfield-Pranav-2018-10-30/config.yaml*\n", + "\n", + "path_config_file = str(Path.cwd() / \"openfield-Pranav-2018-10-30\" / \"config.yaml\")\n", + "deeplabcut.load_demo_data(path_config_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "ROlflqQLywnP" + }, + "outputs": [], + "source": [ + "# [OPTIONAL] Perhaps plot the labels to see how the frames were annotated:\n", + "# (note, this project was created in Linux, so you might have an error in Windows, but this is an optional step)\n", + "\n", + "deeplabcut.check_labels(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "h9H7eqDLywnV" + }, + "source": [ + "## Start training of Feature Detectors\n", + "\n", + "This function trains the network for a specific shuffle of the training dataset. The user can set various parameters in `/openfield-Pranav-2018-10-30/dlc-models-pytorch/.../pytorch_config.yaml`. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", + "\n", + "Training can be stopped at any time. Note that the weights are only stored every 'save_epochs' epochs. For this demo the state it is advisable to store & display the progress very often. In practice this is inefficient. You should see the model start converging around 50 to 60 epochs; you can continue training it longer to improve performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jg96O2acywnW", + "scrolled": true + }, + "outputs": [], + "source": [ + "# notice the variables \"save_epochs\" and \"displayiters\" that can be set in the function\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note, that if it reaches the end or you stop it (by hitting \"stop\" or by CTRL+C), \n", + "you will see an \"KeyboardInterrupt\" error, but you can ignore this!**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CCzxVT_gywnc" + }, + "source": [ + "## Evaluate a trained network\n", + "\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular training state (snapshot) or on all the states. The network is evaluated on the data set (images) and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**.\n", + "\n", + "You can change various parameters in the ```config.yaml``` file of this project. For evaluation all the model descriptors (Task, TrainingFraction, Date etc.) are important. For the evaluation one can change pcutoff. This cutoff also influences how likely estimated positions need to be so that they are shown in the plots. One can furthermore, change the colormap and dotsize for those graphs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "kuprPKDdywne", + "scrolled": false + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(path_config_file, plotting=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*NOTE: depending on your set up sometimes you get some \"matplotlib errors, but these are not important*\n", + "\n", + "Now you can go check out the images. Given the limited data input and it took ~20 mins to test this out, it is not meant to track well, so don't be alarmed. This is just to get you familiar with the workflow... " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "XeqYWGaXywnj" + }, + "source": [ + "## Analyzing videos\n", + "This function extracts the pose based on a trained network from videos. The user can choose the trained network - by default the most recent snapshot is used to analyse the videos. However, the user can also specify the snapshot index for the variable **snapshotindex** in the **config.yaml** file).\n", + "\n", + "The results are stored in hd5 file in the same directory, where the video resides. The pose array (pose vs. frame index) can also be exported as csv file (set flag to...). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Vv9iHHLlywnl" + }, + "outputs": [], + "source": [ + "videofile_path = str(Path(path_config_file).parent / \"videos\" / \"m3v1mp4.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CFbPPD4hywnq", + "scrolled": false + }, + "outputs": [], + "source": [ + "print(\"Start analyzing the video!\")\n", + "# our demo video on a CPU with take ~5 min to analze! GPU is much faster!\n", + "deeplabcut.analyze_videos(path_config_file, [videofile_path])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "QQ3T3oykywnw" + }, + "source": [ + "## Create labeled video\n", + "\n", + "This function is for the visualization purpose and can be used to create a video in .mp4 format with the predicted labels. This video is saved in the same directory, where the (unlabeled) video resides. \n", + "\n", + "Various parameters can be set with regard to the colormap and the dotsize. The parameters of the " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "GhI9KLs4ywn0", + "scrolled": true + }, + "outputs": [], + "source": [ + "deeplabcut.create_labeled_video(path_config_file, [videofile_path])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "IErvm1K5ywn5" + }, + "source": [ + "## Plot the trajectories of the analyzed videos\n", + "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color. The underlying functions can easily be customized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "mP2useJgywn7", + "scrolled": false + }, + "outputs": [], + "source": [ + "%matplotlib notebook\n", + "deeplabcut.plot_trajectories(\n", + " path_config_file,\n", + " [videofile_path],\n", + " showfigures=True,\n", + ")\n", + "\n", + "# These plots are interactive and can be customized (see https://matplotlib.org/)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "a5nOaWzXywoB" + }, + "source": [ + "## Extract outlier frames, where the predictions are off.\n", + "\n", + "This is optional step allows to add more training data when the evaluation results are poor. In such a case, the user can use the following function to extract frames where the labels are incorrectly predicted. Make sure to provide the correct value of the \"iterations\" as it will be used to create the unique directory where the extracted frames will be saved." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "RJGiDKuUywoC", + "scrolled": true + }, + "outputs": [], + "source": [ + "deeplabcut.extract_outlier_frames(path_config_file, [videofile_path])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "jHjpscPcywoG" + }, + "source": [ + "The user can run this iteratively, and (even) extract additional frames from the same video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "uaNUm3NSywoH" + }, + "source": [ + "## Manually correct labels\n", + "\n", + "This step allows the user to correct the labels in the extracted frames. Navigate to the folder corresponding to the video 'm3v1mp4' and use the GUI as described in the protocol to update the labels.\n", + "\n", + "For documentation regarding the GUI, [look at the docs for `napari-deeplabcut`](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) - and specifically _\"3. Refining labels – the image folder contains a machinelabels-iter<#>.h5 file.\"_!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "OJDvJMcrywoI" + }, + "outputs": [], + "source": [ + "deeplabcut.refine_labels(path_config_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Y7efellnywoT" + }, + "outputs": [], + "source": [ + "# Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", + "deeplabcut.check_labels(path_config_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "mcuqoeRbywoL" + }, + "outputs": [], + "source": [ + "# Now merge datasets (once you refined all frames)\n", + "deeplabcut.merge_datasets(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WRB9KgGsywoP" + }, + "source": [ + "## Create a new iteration of training dataset, check it and train...\n", + "\n", + "Following the refine labels, append these frames to the original dataset to create a new iteration of training dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "NGHghXfdywoQ" + }, + "outputs": [], + "source": [ + "deeplabcut.create_training_dataset(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8fhL6nG2ywoW" + }, + "source": [ + "Now one can train the network again... (with the expanded data set). We can continue training from the snapshot we already have by using the `snapshot_path` argument - instead of training the model from scratch, it will load the weights we already have and fine-tune them!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "cAUxg5sgywoX" + }, + "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", + ")\n", + "\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=10,\n", + " batch_size=8,\n", + " snapshot_path=snapshot_path,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "name": "Demo-labeledexample-MouseReaching.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-02-28", + "last_metadata_updated": "2026-03-06" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.11" + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_napari.ipynb similarity index 76% rename from examples/Demo_yourowndata.ipynb rename to examples/JUPYTER/Demo_napari.ipynb index e286f2e6fd..764390965d 100644 --- a/examples/Demo_yourowndata.ipynb +++ b/examples/JUPYTER/Demo_napari.ipynb @@ -7,8 +7,10 @@ "id": "RK255E7YoEIt" }, "source": [ - "# DeepLabCut Toolbox\n", - "https://github.com/AlexEMG/DeepLabCut\n", + "# DeepLabCut Toolbox, labeling with napari\n", + "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1d409ffe-c9f4-47e1-bde2-3010c1c40455/naparidlc.png?format=500w)\n", "\n", "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", "This shows the most simple code to do so, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", @@ -16,7 +18,7 @@ "This notebook illustrates how to:\n", "- create a project\n", "- extract training frames\n", - "- label the frames\n", + "- label the frames [NEW! with napari]\n", "- plot the labeled images\n", "- create a training set\n", "- train a network\n", @@ -45,14 +47,14 @@ "source": [ "## Create a new project\n", "\n", - "It is always good idea to keep the projects seperate if you want to use different networks to analze your data. You should use one project if you are tracking similar subjects/items even if in different environments. This function creates a new project with sub-directories and a basic configuration file in the user defined directory otherwise the project is created in the current working directory.\n", + "It is always good idea to keep the projects separate if you want to use different networks to analze your data. You should use one project if you are tracking similar subjects/items even if in different environments. This function creates a new project with sub-directories and a basic configuration file in the user defined directory otherwise the project is created in the current working directory.\n", "\n", "You can always add new videos (for lableing more data) to the project at any stage of the project. " ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -65,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -73,15 +75,20 @@ }, "outputs": [], "source": [ - "task='Reaching' # Enter the name of your experiment Task\n", - "experimenter='Mackenzie' # Enter the name of the experimenter\n", - "video=['videos/video1.avi','videos/video2.avi'] # Enter the paths of your videos OR FOLDER you want to grab frames from.\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", "\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", - "# You could also enter this manually (e.g. if the project is already created and you want to pick up, where you stopped...)\n", - "#path_config_file = '/home/Mackenzie/Reaching/config.yaml' # Enter the path of the config file that was just created from the above step (check the folder)" + "# 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", + "# 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\"" ] }, { @@ -101,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -129,7 +136,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -137,23 +144,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) " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#AND/OR:\n", - "#SELECT RARE EVENTS MANUALLY:\n", - "%gui wx\n", - "deeplabcut.extract_frames(path_config_file,'manual')" + "# AUTOMATIC:\n", + "deeplabcut.extract_frames(path_config_file)" ] }, { @@ -170,7 +164,22 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "# 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)." + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -178,8 +187,11 @@ }, "outputs": [], "source": [ - "%gui wx\n", - "deeplabcut.label_frames(path_config_file)" + "# napari will pop up! Please go to plugin > deeplabcut to start:\n", + "%gui qt6\n", + "import napari\n", + "\n", + "napari.Viewer()" ] }, { @@ -196,7 +208,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -204,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" ] }, { @@ -230,14 +242,14 @@ "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**. For most all use cases we have seen, the defaults are perfectly fine.\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pytorch_config.yaml**. For most all use cases we have seen, the defaults are perfectly fine.\n", "\n", "Now it is the time to start training the network!" ] }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -247,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!" ] }, { @@ -264,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -283,13 +295,13 @@ }, "source": [ "## Start evaluating\n", - "This funtion evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -315,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -323,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\")" ] }, { @@ -342,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -351,7 +363,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -359,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" ] }, { @@ -375,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -383,8 +395,12 @@ }, "outputs": [], "source": [ - "%gui wx\n", - "deeplabcut.refine_labels(path_config_file)" + "# 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()" ] }, { @@ -406,7 +422,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -414,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)" ] @@ -432,7 +448,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -451,7 +467,7 @@ }, "source": [ "## Create labeled video\n", - "This funtion is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", "\n", "THIS HAS MANY FUN OPTIONS! \n", "\n", @@ -462,7 +478,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -471,7 +487,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -479,7 +495,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -495,7 +511,7 @@ }, { "cell_type": "code", - "execution_count": 0, + "execution_count": null, "metadata": { "colab": {}, "colab_type": "code", @@ -504,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)" ] } ], @@ -515,10 +531,15 @@ "provenance": [], "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [conda env:DLC2]", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "conda-env-DLC2-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -530,7 +551,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/JUPYTER/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_yourowndata.ipynb new file mode 100644 index 0000000000..8a62f53afe --- /dev/null +++ b/examples/JUPYTER/Demo_yourowndata.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "RK255E7YoEIt" + }, + "source": [ + "# DeepLabCut Toolbox\n", + "\n", + "\n", + "Some resources that can be useful:\n", + "\n", + "- [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)\n", + "- [DeepLabCut's Documentation: User Guide for Single Animal projects](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide.html)\n", + "\n", + "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", + "This shows the most simple code to do so, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", + "\n", + "This notebook illustrates how to:\n", + "- create a project\n", + "- extract training frames\n", + "- label the frames\n", + "- plot the labeled images\n", + "- create a training set\n", + "- train a network\n", + "- evaluate a network\n", + "- analyze a novel video\n", + "- create an automatically labeled video \n", + "- plot the trajectories\n", + "\n", + "This notebook demonstrates the necessary steps to use DeepLabCut for your own project.\n", + "\n", + "This shows the most simple code to do so, but many of the functions have additional features, so please check out the overview & the protocol paper!\n", + "\n", + "Nath\\*, Mathis\\* et al.: Using DeepLabCut for markerless pose estimation during behavior across species. Nature Protocols, 2019.\n", + "\n", + "Paper: https://www.nature.com/articles/s41596-019-0176-0\n", + "\n", + "Pre-print: https://www.biorxiv.org/content/biorxiv/early/2018/11/24/476531.full.pdf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9Uoz9mdPoEIy" + }, + "source": [ + "## Create a new project\n", + "\n", + "It is always good idea to keep the projects separate if you want to use different networks to analze your data. You should use one project if you are tracking similar subjects/items even if in different environments. This function creates a new project with sub-directories and a basic configuration file in the user defined directory otherwise the project is created in the current working directory.\n", + "\n", + "You can always add new videos (for lableing more data) to the project at any stage of the project. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jqLZhp7EoEI0" + }, + "outputs": [], + "source": [ + "import deeplabcut" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "c9DjG55FoEI7" + }, + "outputs": [], + "source": [ + "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", + "\n", + "path_config_file = deeplabcut.create_new_project(\n", + " task,\n", + " experimenter,\n", + " video,\n", + " copy_videos=True,\n", + ")\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\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\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Now, go edit the config.yaml file that was created! \n", + "Add your body part labels, edit the number of frames to extract per video, etc. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Note that you can see more information about ANY function by adding a ? at the end, i.e. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "deeplabcut.extract_frames?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "0yXW0bx1oEJA" + }, + "source": [ + "## Extract frames from videos \n", + "A key point for a successful feature detector is to select diverse frames, which are typical for the behavior you study that should be labeled.\n", + "\n", + "This function selects N frames either uniformly sampled from a particular video (or folder) ('uniform'). Note: this might not yield diverse frames, if the behavior is sparsely distributed (consider using kmeans), and/or select frames manually etc.\n", + "\n", + "Also make sure to get select data from different (behavioral) sessions and different animals if those vary substantially (to train an invariant feature detector).\n", + "\n", + "Individual images should not be too big (i.e. < 850 x 850 pixel). Although this can be taken care of later as well, it is advisable to crop the frames, to remove unnecessary parts of the frame as much as possible.\n", + "\n", + "Always check the output of cropping. If you are happy with the results proceed to labeling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "t1ulumCuoEJC" + }, + "outputs": [], + "source": [ + "%matplotlib inline\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)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Gjn6ZDonoEJH" + }, + "source": [ + "## Label the extracted frames\n", + "\n", + "Only videos in the config file can be used to extract the frames. Extracted labels for each video are stored in the project directory under the subdirectory **'labeled-data'**. Each subdirectory is named after the name of the video. The toolbox has a labeling toolbox which could be used for labeling. \n", + "\n", + "Check out [our `napari-deeplabcut` docs](https://github.com/DeepLabCut/napari-deeplabcut/tree/main) for more information about labelling!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "iyROSOiEoEJI" + }, + "outputs": [], + "source": [ + "# napari will pop up!\n", + "# Please go to plugin > deeplabcut to start\n", + "# then, drag-and-drop the project configuration file into the viewer (the value of path_config_file)\n", + "# finally, drop the folder containing the images (in 'labeled-data') in the viewer\n", + "\n", + "%gui qt6\n", + "import napari\n", + "\n", + "napari.Viewer()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "vim95ZvkPSeN" + }, + "source": [ + "## Check the labels\n", + "\n", + "[OPTIONAL] Checking if the labels were created and stored correctly is beneficial for training, since labeling is one of the most critical parts for creating the training dataset. The DeepLabCut toolbox provides a function `check\\_labels' to do so. It is used as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "NwvgPJouPP2O" + }, + "outputs": [], + "source": [ + "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "of87fOjgPqzH" + }, + "source": [ + "If the labels need adjusted, you can use relauch the labeling GUI to move them around, save, and re-plot!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "xNi9s1dboEJN" + }, + "source": [ + "## Create a training dataset\n", + "\n", + "This function generates the training data information for network training based on the pandas dataframes that hold label information. The user can set the fraction of the training set size (from all labeled image in the hd5 file) in the config.yaml file. While creating the dataset, the user can create multiple shuffles if they want to benchmark the performance (typcailly, 1 is what you will set, so you pass nothing!). \n", + "\n", + "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", + "\n", + "This function also creates new subdirectories under **dlc-models-pytorch** and creates a `pytorch_config.yaml` file, defining the model architecture and containing various parameters used for training the network. For most all use cases we have seen, the defaults are perfectly fine. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", + "\n", + "Now it is the time to start training the network!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "eMeUwgxPoEJP", + "scrolled": true + }, + "outputs": [], + "source": [ + "deeplabcut.create_training_dataset(path_config_file)\n", + "\n", + "# remember, there are several networks you can pick, the default is resnet-50!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "c4FczXGDoEJU" + }, + "source": [ + "## Start training:\n", + "\n", + "The user can set various parameters in `.../project-name/dlc-models-pytorch/.../pytorch_config.yaml`. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n", + "\n", + "This function trains the network for a specific shuffle of the training dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "_pOvDq_2oEJW" + }, + "outputs": [], + "source": [ + "deeplabcut.train_network(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "xZygsb2DoEJc" + }, + "source": [ + "## Start evaluating\n", + "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "nv4zlbrnoEJg" + }, + "outputs": [], + "source": [ + "deeplabcut.evaluate_network(path_config_file, plotting=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OVFLSKKfoEJk" + }, + "source": [ + "## Start Analyzing videos\n", + "This function analyzes the new video. The user can choose the best model from the evaluation results and specify the correct snapshot index for the variable **snapshotindex** in the **config.yaml** file. Otherwise, by default the most recent snapshot is used to analyse the video.\n", + "\n", + "The results are stored in hd5 file in the same directory where the video resides. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Y_LZiS_0oEJl" + }, + "outputs": [], + "source": [ + "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\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "iGu_PdTWoEJr" + }, + "source": [ + "## Extract outlier frames [optional step]\n", + "\n", + "This is an optional step and is used only when the evaluation results are poor i.e. the labels are incorrectly predicted. In such a case, the user can use the following function to extract frames where the labels are incorrectly predicted. This step has many options, so please look at:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "deeplabcut.extract_outlier_frames?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "gkbaBOJVoEJs" + }, + "outputs": [], + "source": [ + "deeplabcut.extract_outlier_frames(path_config_file, [\"/videos/video3.avi\"]) # pass a specific video" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8ib0uvhaoEJx" + }, + "source": [ + "## Refine Labels [optional step]\n", + "Following the extraction of outlier frames, the user can use the following function to move the predicted labels to the correct location. Thus augmenting the training dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "n_FpEXtyoEJy" + }, + "outputs": [], + "source": [ + "%gui qt6\n", + "deeplabcut.refine_labels(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**NOTE:** Afterwards, if you want to look at the adjusted frames, you can load them in the main GUI by running: ``deeplabcut.label_frames(path_config_file)``\n", + "\n", + "(you can add a new \"cell\" below to add this code!)\n", + "\n", + "#### Once all folders are relabeled, check the labels again! If you are not happy, adjust them in the main GUI:\n", + "\n", + "``deeplabcut.label_frames(path_config_file)``\n", + "\n", + "Check Labels:\n", + "\n", + "``deeplabcut.check_labels(path_config_file)``" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CHzstWr8oEJ2" + }, + "outputs": [], + "source": [ + "# NOW, merge this with your original data:\n", + "\n", + "deeplabcut.merge_datasets(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "QCHj7qyboEJ6" + }, + "source": [ + "## Create a new iteration of training dataset [optional step]\n", + "Following the refinement of labels and appending them to the original dataset, this creates a new iteration of training dataset. This is automatically set in the config.yaml file, so let's get training!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "ytQoxIldoEJ7" + }, + "outputs": [], + "source": [ + "deeplabcut.create_training_dataset(path_config_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "pCrUvQIvoEKD" + }, + "source": [ + "## Create labeled video\n", + "\n", + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. \n", + "\n", + "THIS HAS MANY FUN OPTIONS! \n", + "\n", + "```python\n", + "deeplabcut.create_labeled_video(\n", + " config,\n", + " videos,\n", + " videotype='avi',\n", + " shuffle=1,\n", + " trainingsetindex=0,\n", + " filtered=False,\n", + " save_frames=False,\n", + " Frames2plot=None,\n", + " delete=False,\n", + " displayedbodyparts='all',\n", + " codec='mp4v',\n", + " outputframerate=None,\n", + " destfolder=None,\n", + " draw_skeleton=False,\n", + " trailpoints=0,\n", + " displaycropped=False,\n", + ")\n", + "```\n", + "\n", + "So please check:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "deeplabcut.create_labeled_video?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "6aDF7Q7KoEKE" + }, + "outputs": [], + "source": [ + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8GTiuJESoEKH" + }, + "source": [ + "## Plot the trajectories of the analyzed videos\n", + "This function plots the trajectories of all the body parts across the entire video. Each body part is identified by a unique color." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "gX21zZbXoEKJ" + }, + "outputs": [], + "source": [ + "%matplotlib notebook #for making interactive plots.\n", + "deeplabcut.plot_trajectories(path_config_file, videofile_path)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "name": "Demo-yourowndata.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.11" + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/Docker_TrainNetwork_VideoAnalysis.ipynb b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb similarity index 61% rename from examples/Docker_TrainNetwork_VideoAnalysis.ipynb rename to examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb index c2579cf5fa..c9808a0f35 100644 --- a/examples/Docker_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb @@ -8,7 +8,7 @@ }, "source": [ "# DeepLabCut Toolbox - Docker\n", - "https://github.com/AlexEMG/DeepLabCut\n", + "https://github.com/DeepLabCut/DeepLabCut\n", "\n", "Nath\\*, Mathis\\* et al. *Using DeepLabCut for markerless pose estimation during behavior across species*\n", "\n", @@ -56,25 +56,11 @@ }, "outputs": [], "source": [ - "import tensorflow as tf\n", - "tf.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Pm_PC1Q8lRrH" - }, - "outputs": [], - "source": [ - "#let's make sure we see a GPU:\n", - "#tf.test.gpu_device_name()\n", - "#or\n", - "from tensorflow.python.client import device_lib\n", - "device_lib.list_local_devices()" + "import torch\n", + "\n", + "# Let's make sure we see a GPU:\n", + "print(torch.__version__)\n", + "print(torch.cuda.is_available())" ] }, { @@ -94,10 +80,11 @@ }, "outputs": [], "source": [ - "#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", + "# 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", - "os.environ[\"DLClight\"]=\"True\"" + "\n", + "os.environ[\"DLClight\"] = \"True\"" ] }, { @@ -112,8 +99,7 @@ "outputs": [], "source": [ "# now we are ready to train!\n", - "import deeplabcut\n", - "deeplabcut.__version__" + "import deeplabcut" ] }, { @@ -133,7 +119,8 @@ }, "outputs": [], "source": [ - "path_config_file = '/home/mackenzie/DEEPLABCUT/DeepLabCut2.0/examples/Reaching-Mackenzie-2018-08-30/config.yaml' #change to yours!" + "# change to yours!\n", + "path_config_file = \"/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"" ] }, { @@ -155,11 +142,12 @@ }, "source": [ "## Create a training dataset\n", - "This function generates the training data information for DeepCut (which requires a mat file) based on the pandas dataframes that hold label information. The user can set the fraction of the training set size (from all labeled image in the hd5 file) in the config.yaml file. While creating the dataset, the user can create multiple shuffles. \n", + "\n", + "This function generates the training data required for DeepLabCut. The user can set the fraction of the training set size (from all labeled images in the hd5 file) in the `config.yaml` file. While creating the dataset, the user can create multiple shuffles. \n", "\n", "After running this script the training dataset is created and saved in the project directory under the subdirectory **'training-datasets'**\n", "\n", - "This function also creates new subdirectories under **dlc-models** and appends the project config.yaml file with the correct path to the training and testing pose configuration file. These files hold the parameters for training the network. Such an example file is provided with the toolbox and named as **pose_cfg.yaml**." + "This function also creates new subdirectories under **dlc-models-pytorch** and creates a `pytorch_config.yaml` file, defining the model architecture and containing various parameters used for training the network. For most all use cases we have seen, the defaults are perfectly fine. For more information about the variables that can be set, check out the [docs](https://deeplabcut.github.io/DeepLabCut/docs/pytorch/pytorch_config.html)!\n" ] }, { @@ -168,16 +156,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.create_training_dataset(path_config_file,Shuffles=[1])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### now go edit the pose_cfg.yaml to make display_iters: low (i.e. 10), and save_iters: 500 (for demo's)\n", - "\n", - "Now it is the time to start training the network!" + "deeplabcut.create_training_dataset(path_config_file, Shuffles=[1])" ] }, { @@ -202,54 +181,18 @@ }, "outputs": [], "source": [ - "#reset in case you started a session before...\n", - "#tf.reset_default_graph()\n", + "deeplabcut.train_network(\n", + " path_config_file,\n", + " shuffle=1,\n", + " save_epochs=2,\n", + " displayiters=5,\n", + ")\n", "\n", - "deeplabcut.train_network(path_config_file, shuffle=1, saveiters=1000, displayiters=10)\n", + "# This will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it\n", + "# hits the end (default, 200 epochs).\n", "\n", - "#this will run until you stop it (CTRL+C), or hit \"STOP\" icon, or when it hits the end (default, 1.3M iterations). \n", - "#Whichever you chose, you will see what looks like an error message, but it's not an error - don't worry....\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Firstly, if the above cell ran, you can stop it with \"stop\" or cntrl-C; you will get a Keyboard Interrupt error (this is fine!)\n", - "\n", - "### A couple tips for possible troubleshooting (1): \n", - "\n", - "if you get **permission errors** when you run this step (above), first check if the weights downloaded. As some docker containers might not have privileges for this (it can be user specific). They should be under 'init_weights' (see path in the pose_cfg.yaml file). You can enter the DOCKER in the terminal:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "see more here: https://github.com/MMathisLab/Docker4DeepLabCut2.0#using-the-docker-for-training-and-video-analysis" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can \"cd\" in the terminal to this location! i.e. copy and paste this in: **\"cd usr/local/lib/python3.6/dist-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/\n", - "\"** \n", - "\n", - "And if you type \"ls\" to see the list of files, you should see the resnet:\n", - "**resnet_v1_50.ckpt**\n", - "\n", - "If it is not there, run **\"sudo download.sh\"**\n", - "then change the permissions: **\"sudo chown yourusername:yourusername resnet_v1_50.ckpt\"**\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Troubleshooting (2): \n", - "if it appears the training does not start (i.e. \"Starting training...\" does not print immediately),\n", - "then you have another session running on your GPU. Go check \"nvidia-smi\" and look at the process names. You can only have 1 per GPU!)" + "# 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...." ] }, { @@ -260,8 +203,8 @@ }, "source": [ "## Start evaluating\n", - "This funtion evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", - "and stores the results as .csv file in a subdirectory under **evaluation-results**" + "This function evaluates a trained model for a specific shuffle/shuffles at a particular state or all the states on the data set (images)\n", + "and stores the results as .csv file in a subdirectory under **evaluation-results-pytorch**" ] }, { @@ -277,7 +220,8 @@ "source": [ "deeplabcut.evaluate_network(path_config_file)\n", "\n", - "# Here you want to see a low pixel error! Of course, it can only be as good as the labeler, so be sure your labels are good!" + "# Here you want to see a low pixel error! Of course, it can only\n", + "# be as good as the labeler, so be sure your labels are good!" ] }, { @@ -317,8 +261,10 @@ }, "outputs": [], "source": [ - "videofile_path = ['/home/mackenzie/DEEPLABCUT/DeepLabCut2.0/examples/Reaching-Mackenzie-2018-08-30/videos/MovieS2_Perturbation_noLaser_compressed.avi'] #Enter the list of videos to analyze.\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path)" + "videofile_path = [\n", + " \"/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/videos/MovieS2_Perturbation_noLaser_compressed.avi\"\n", + "] # Enter the list of videos to analyze.\n", + "deeplabcut.analyze_videos(path_config_file, videofile_path)" ] }, { @@ -329,7 +275,7 @@ }, "source": [ "## Create labeled video\n", - "This funtion is for visualiztion purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " + "This function is for visualization purpose and can be used to create a video in .mp4 format with labels predicted by the network. This video is saved in the same directory where the original video resides. " ] }, { @@ -343,7 +289,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -369,10 +315,9 @@ "outputs": [], "source": [ "%matplotlib notebook \n", - "#for making interactive plots.\n", - "#deeplabcut.plot_trajectories(path_config_file,videofile_path, plotting=True)\n", - "\n", - "deeplabcut.plot_trajectories(path_config_file,videofile_path,showfigures=True)" + "# for making interactive plots.\n", + "# deeplabcut.plot_trajectories(path_config_file, videofile_path, plotting=True)\n", + "deeplabcut.plot_trajectories(path_config_file, videofile_path, showfigures=True)" ] } ], @@ -386,8 +331,13 @@ "toc_visible": true, "version": "0.3.2" }, + "deeplabcut": { + "ignore": false, + "last_content_updated": "2025-09-16", + "last_metadata_updated": "2026-03-06" + }, "kernelspec": { - "display_name": "Python [default]", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -401,7 +351,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.11.11" }, "varInspector": { "cols": { diff --git a/examples/README.md b/examples/README.md index 6e790be0ec..ca87f2b722 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,38 +1,35 @@ -# Demo Jupyter & Colaboratory Notebooks: +# Demo Jupyter & Colaboratory Notebooks: DLC We provide a Project Manager GUI that will walk you through the major steps and options of the DeepLabCut Toolbox. However, there are more options and features that can be accessed by running the code in an interactive environment, such as Jupyter*. Moreover, if you don't have a GPU, you can create your project on any computer, then move your project to the cloud to use GPUs. To do this, we provide you with Google Colaboratory Notebooks (see [Demo using Google Colaboratory below](/examples#demo-deeplabcut-training-and-analysis-on-google-colaboratory-with-googles-gpus)). -## Demo 1: run DeepLabCut [on our open-field data](Demo_labeledexample_Openfield.ipynb) +## Demo 1: run DeepLabCut [on our open-field data](JUPYTER/Demo_labeledexample_Openfield.ipynb) - This will give you a feel of the workflow for DeepLabCut. Follow the instructions inside the notebook! -Note, the notebooks with labeled data: [reaching data](Demo_labeledexample_MouseReaching.ipynb), or [open-field data](Demo_labeledexample_Openfield.ipynb) can be run on a CPU, GPU, etc. The one with the open-field data even achieves good/okay results, when trained for half an hour on a GPU! (To note, this is NOT the full dataset that was used in Mathis et al, 2018) +Note, the notebooks with labeled data: [reaching data](JUPYTER/Demo_labeledexample_MouseReaching.ipynb), or [open-field data](JUPYTER/Demo_labeledexample_Openfield.ipynb) can be run on a CPU, GPU, etc. The one with the open-field data even achieves good/okay results, when trained for half an hour on a GPU! (To note, this is NOT the full dataset that was used in Mathis et al, 2018) -## Demo 2: Set up DeepLabCut on [your own data](Demo_yourowndata.ipynb) +## Demo 2: Set up DeepLabCut on [your own data](JUPYTER/Demo_yourowndata.ipynb) - Now that you're a master of the demos, this Notebook walks you through how to build your own pipeline: - Create a new project - Label new data - Then, either use your CPU, or your GPU (the Notebook will guide you at this junction), to train, analyze and perform some basic analysis of your data. -For GPU-based training and analysis you will need to switch to either our [supplied Docker container](https://github.com/MMathisLab/Docker4DeepLabCut2.0), and modify the [Docker Demo Notebook](Docker_TrainNetwork_VideoAnalysis.ipynb) for your project, or you need to [install TensorFlow with GPU support](/docs/installation.md) in an Anaconda Env, or use Google Colab, more below: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AlexEMG/DeepLabCut/blob/master/examples/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb) +For GPU-based training and analysis you will need to switch to either our [supplied Docker container](https://deeplabcut.github.io/DeepLabCut/docs/docker.html), or you need to [install your local GPU](https://deeplabcut.github.io/DeepLabCut/docs/recipes/installTips.html?highlight=gpu#how-to-confirm-that-your-gpu-is-being-used-by-deeplabcut) in an Anaconda Env, or use Google Colab, more below: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb) -## Demo 3: Run DeepLabCut on a [GPU in Docker (linux only)](Docker_TrainNetwork_VideoAnalysis.ipynb) - - This requires the [DeepLabCut Docker](https://github.com/MMathisLab/Docker4DeepLabCut2.0)! +## Demo 3: DeepLabCut training and analysis on Google Colaboratory (with Google's GPUs!): -## Demo 4: DeepLabCut training and analysis on Google Colaboratory (with Google's GPUs!): +We suggest making a "Fork" of this repo, git clone or download the folder into your google drive, then linking your google account to your GitHub (you'll see how to do this in the Notebook below). Then you can edit the Notebooks for your own data too (just put https://colab.research.google.com/ in front of the web address of your own repo). -We suggest making a "Fork" of this repo, git clone or download the folder into your google drive, then linking your google account to your GitHub (you'll see how to do this in the Notebook below). Then you can edit the Notebook for your own data too (just put https://colab.research.google.com/ in front of the web address of your own repo) +- You can use Google [Colaboratory](https://colab.research.google.com) to demo running DeepLabCut on our data. Here is an example colab-ready Jupyter Notebook for the open field data, which you can launch by clicking the badge below: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb) -- You can use Google [Colaboratory](https://colab.research.google.com) to demo running DeepLabCut on our data. Here is an example colab-ready Jupyter Notebook for the open field data, which you can launch by clicking the badge below: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AlexEMG/DeepLabCut/blob/master/examples/COLAB_DEMO_mouse_openfield.ipynb) - -- Using Colab on your data for the training and analysis of new videos, i.e. the parts that need a GPU! -[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AlexEMG/DeepLabCut/blob/master/examples/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb) +- Using Colab on your data for the training and analysis of new videos, i.e. the parts that need a GPU! +[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb) 1. Click Open in Colab to launch the notebook. 2. Make the notebook live by clicking 'Connect' in the Colab toolbar, and then click "Runtime > Change Runtime Type > and select Python3 and GPU as your hardware. Follow the instructions in the Notebook. -3. Be aware, they often don't let you run on their GPUs for very long, so make sure your ``save_inters`` variable is low for this setting. +3. Be aware, they often don't let you run on their GPUs for very long (>6 hrs) without a Pro account, so make sure your ``save_inters`` variable is lower for this setting. Here is a demo of us using the Colab Notebooks: https://www.youtube.com/watch?v=qJGs8nxx80A & https://www.youtube.com/watch?v=j13aXxysI2E @@ -45,7 +42,7 @@ Ready to take your pose estimation to a new dimension? As of 2.0.7+ we support 3 ## Using the DLC Model Zoo: -We provide a COLAB notebook to use the growing number of networks that are trained on specific animals/scenarios. Read more here: http://www.mousemotorlab.org/dlc-modelzoo. This code will also create a new project folder so you can refine, add new bodyparts or label other objects, and re-train. Launch COLAB here: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AlexEMG/DeepLabCut/blob/master/examples/COLAB_DLC_ModelZoo.ipynb) +We provide a COLAB notebook to use the growing number of networks that are trained on specific animals/scenarios. Read more here: http://www.mousemotorlab.org/dlc-modelzoo. This code will also create a new project folder so you can refine, add new bodyparts or label other objects, and re-train. Launch COLAB here: [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/DeepLabCut/DeepLabCut/blob/master/examples/COLAB/COLAB_DLC_ModelZoo.ipynb) ## Using Python/iPython: @@ -54,13 +51,12 @@ All of DeepLabCut can be run from an ipython console in the program **terminal** We also have some video tutorials to demonstrate how we use Anaconda and Docker via the terminal: https://www.youtube.com/watch?v=7xwOhUcIGio & https://www.youtube.com/watch?v=bgfnz1wtlpo - - + + ## * You can download DeepLabCut & associated files: -To have a copy of DeepLabCut on your own computer, we recommend using **Anaconda to install Python and Jupyter Notebooks, see the [Installation](/docs/installation.md) page**. Then, on your local machine using these notebooks to guide you, you can (1) demo our labeled data (or create your own), (2) create a project, extract frames to lablel, use the GUI to label, and create a training set for the neural network(s). +To have a copy of DeepLabCut on your own computer, we recommend using **Anaconda to install Python and Jupyter Notebooks, see the [Installation](/docs/installation.md) page**. Then, on your local machine using these notebooks to guide you, you can (1) demo our labeled data (or create your own), (2) create a project, extract frames to label, use the GUI to label, and create a training set for the neural network(s). We suggest making a "Fork" of this repo and/or then place DeepLabCut files in a folder: -``git clone https://github.com/AlexEMG/DeepLabCut`` +``git clone https://github.com/DeepLabCut/DeepLabCut`` so you can access it locally with **Anaconda.** You can also click the "download" button, rather than using ``git``. Then you can edit the Notebooks as you like! - diff --git a/examples/openfield-Pranav-2018-10-30/config.yaml b/examples/openfield-Pranav-2018-10-30/config.yaml index 64c2ce17b2..ed8c31fdf3 100644 --- a/examples/openfield-Pranav-2018-10-30/config.yaml +++ b/examples/openfield-Pranav-2018-10-30/config.yaml @@ -2,10 +2,17 @@ Task: openfield scorer: Pranav date: Oct30 +multianimalproject: +identity: + # Project path (change when moving around) project_path: WILL BE AUTOMATICALLY UPDATED BY DEMO CODE +# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) +engine: pytorch + + # Annotation data set configuration (and individual video cropping parameters) video_sets: WILL BE AUTOMATICALLY UPDATED BY DEMO CODE: @@ -16,23 +23,33 @@ bodyparts: - rightear - tailbase + +# Fraction of video to start/stop when extracting frames for labeling/refinement start: 0 stop: 1 numframes2pick: 20 + # Plotting configuration +skeleton: [] +skeleton_color: black pcutoff: 0.4 dotsize: 8 alphavalue: 0.7 colormap: jet + # Training,Evaluation and Analysis configuration TrainingFraction: - 0.95 iteration: 0 default_net_type: resnet_50 +default_augmenter: imgaug snapshotindex: -1 +detector_snapshotindex: -1 batch_size: 4 +detector_batch_size: 1 + # Cropping Parameters (for analysis and outlier frame detection) cropping: false @@ -42,8 +59,18 @@ x2: 640 y1: 277 y2: 624 + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) corner2move2: - 50 - 50 move2corner: true + + +# Conversion tables to fine-tune SuperAnimal weights +SuperAnimalConversionTables: + superanimal_topviewmouse: + snout: nose + leftear: left_ear + rightear: right_ear + tailbase: tail_base diff --git a/examples/stereo_example.zip b/examples/stereo_example.zip new file mode 100644 index 0000000000..85b591cbe0 Binary files /dev/null and b/examples/stereo_example.zip differ diff --git a/examples/test.sh b/examples/test.sh index 2f38051988..b9be0a511d 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -6,14 +6,14 @@ rm -r OUT cd .. pip uninstall deeplabcut python3 setup.py sdist bdist_wheel -pip install dist/deeplabcut-2.2rc1-py3-none-any.whl +pip install dist/deeplabcut-3.0.0-none-any.whl cd examples -python3 testscript.py +python3 testscript_tensorflow_single_animal.py python3 testscript_3d.py #does not work in container #python3 testscript_mobilenets.py -python3 testscript_multianimal.py +python3 testscript_tensorflow_multi_animal.py #python3 testscript_openfielddata_netcomparison.py #python3 testscript_openfielddata_augmentationcomparison.py diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index 2cb3823407..a31c034a9f 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -1,182 +1,189 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# """ DeepLabCut2.0 Toolbox (deeplabcut.org) © A. & M. Mathis Labs -https://github.com/AlexEMG/DeepLabCut +https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for contributors. -https://github.com/AlexEMG/DeepLabCut/blob/master/AUTHORS +https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 This script tests various functionalities in an automatic way. It produces nothing of interest scientifically. """ - -task = "TEST3D" # Enter the name of your experiment Task -scorer = "Alex" # Enter the name of the experimenter/labeler -num_cameras = 2 # Enter the number of cameras - -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 -print("Imported DLC!") - -basepath = os.path.dirname(os.path.abspath("testscript_3d.py")) -videoname = "reachingvideo1" - -video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) -] - -folder = "3Dtestviews_videos" -deeplabcut.auxiliaryfunctions.attempttomakefolder(folder) - -# copying demo video from reaching data set and create two "views": -dst_videoname1 = "vid1_camera-1" -dst_videoname2 = "vid1_camera-2" -dst_videoname3 = "long_camera-2" -output1 = os.path.join(basepath, folder, dst_videoname1 + ".avi") -output2 = os.path.join(basepath, folder, dst_videoname2 + ".avi") - -output3 = os.path.join(basepath, folder, dst_videoname3 + ".avi") -shutil.copyfile(video[0], output3) - -vname = "brief" -try: # you need ffmpeg command line interface - subprocess.call( - [ - "ffmpeg", - "-i", - video[0], - "-ss", - "00:00:00", - "-to", - "00:00:00.4", - "-c", - "copy", - output1, - ] - ) - subprocess.call( - [ - "ffmpeg", - "-i", - video[0], - "-ss", - "00:00:00", - "-to", - "00:00:00.4", - "-c", - "copy", - output2, - ] - ) -except: - pass - -""" -# copying demo video from reaching data set and create two "views": -dst_videoname1 = 'vid1_camera-1' -dst_videoname2 = 'vid1_camera-2' -output1 = os.path.join(basepath,folder,dst_videoname1+'.avi') -output2 = os.path.join(basepath,folder,dst_videoname2+'.avi') -shutil.copyfile(video[0], output1) -shutil.copyfile(video[0], output2) -""" -# 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") - -dfolder = None - -print("CREATING 3-D PROJECT") -path_config_file = deeplabcut.create_new_project_3d(task, scorer, num_cameras) - -try: - cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) - cfg["config_file_camera-1"] = config - cfg["shuffle_camera-1"] = 1 - - cfg["config_file_camera-2"] = config - cfg["shuffle_camera-2"] = 2 - - 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! - -""" -# Creating the name of the project -date = dt.today() -month = date.strftime("%B") -day = date.day -d = str(month[0:3]+str(day)) -date = dt.today().strftime('%Y-%m-%d') -project_name = '{pn}-{exp}-{date}-{triangulate}'.format(pn=task, exp=scorer, date=date,triangulate='3d') -""" -project_name = path_config_file.split(os.sep)[-2] - -os.chdir(os.path.join(basepath, project_name, "calibration_images")) -# Dowloading the calibration images -url = "http://www.vision.caltech.edu/bouguetj/calib_doc/htmls/stereo_example.zip" -file_name = "stereo_example.zip" -with urllib.request.urlopen(url) as response, open(file_name, "wb") as out_file: - shutil.copyfileobj(response, out_file) -file_name = os.path.join( - basepath, project_name, "calibration_images", "stereo_example.zip" -) -with zipfile.ZipFile(file_name) as zf: - zf.extractall() - -# Deleting unneccesary images; the ones whose corners are not detected and .mat files -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 -cam1_images = glob.glob(os.path.join(cwd, "left*.jpg")) -cam2_images = glob.glob(os.path.join(cwd, "right*.jpg")) -# Sorting images -cam1_images.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) -cam2_images.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) -for idx, name in enumerate(cam1_images): - os.rename( - name, os.path.join(cwd, str("camera-1_" + "{0:0=2d}".format(idx + 1) + ".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")) - ) - -# Removing some of the images where the corner was not detected -[os.remove(file) for file in glob.glob(os.path.join(cwd, "*06.jpg"))] -[os.remove(file) for file in glob.glob(os.path.join(cwd, "*01.jpg"))] - -print("CALIBRATING THE CAMERAS") -deeplabcut.calibrate_cameras(path_config_file, calibrate=True) - -print("CHECKING FOR UNDISTORTION") -deeplabcut.check_undistortion(path_config_file) - -print("TRIANGULATING") -video_dir = os.path.join(basepath, folder) -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) - -# output_path = [os.path.join(basepath,folder)] -# deeplabcut.create_labeled_video_3d(path_config_file,output_path,start=5,end=10) - -print("ALL DONE!!! - default 3D cases are functional.") +import deeplabcut + +if __name__ == "__main__": + print("Imported DLC!") + task = "TEST3D" # Enter the name of your experiment Task + scorer = "Alex" # Enter the name of the experimenter/labeler + num_cameras = 2 # Enter the number of cameras + + basepath = str(Path(os.path.realpath(__file__)).parents[0]) + videoname = "reachingvideo1" + video = [ + os.path.join( + basepath, + "Reaching-Mackenzie-2018-08-30", + "videos", + videoname + ".avi", + ) + ] + + folder = os.path.join(basepath, "3Dtestviews_videos") + deeplabcut.auxiliaryfunctions.attempt_to_make_folder(folder) + + # copying demo video from reaching data set and create two "views": + dst_videoname1 = "vid1_camera-1" + dst_videoname2 = "vid1_camera-2" + dst_videoname3 = "long_camera-2" + output1 = os.path.join(folder, dst_videoname1 + ".avi") + output2 = os.path.join(folder, dst_videoname2 + ".avi") + output3 = os.path.join(folder, dst_videoname3 + ".avi") + shutil.copyfile(video[0], output3) + + vname = "brief" + try: # you need ffmpeg command line interface + subprocess.call( + [ + "ffmpeg", + "-i", + video[0], + "-ss", + "00:00:00", + "-to", + "00:00:00.4", + "-c", + "copy", + output1, + ] + ) + subprocess.call( + [ + "ffmpeg", + "-i", + video[0], + "-ss", + "00:00:00", + "-to", + "00:00:00.4", + "-c", + "copy", + output2, + ] + ) + except Exception: + pass + + """ + # copying demo video from reaching data set and create two "views": + dst_videoname1 = 'vid1_camera-1' + dst_videoname2 = 'vid1_camera-2' + output1 = os.path.join(basepath,folder,dst_videoname1+'.avi') + output2 = os.path.join(basepath,folder,dst_videoname2+'.avi') + shutil.copyfile(video[0], output1) + shutil.copyfile(video[0], output2) + """ + # checking if 2d test project is available + 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 + + dfolder = None + + print("CREATING 3-D PROJECT") + path_config_file = deeplabcut.create_new_project_3d(task, scorer, num_cameras) + + try: + cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) + cfg["config_file_camera-1"] = config + cfg["shuffle_camera-1"] = 1 + + cfg["config_file_camera-2"] = config + cfg["shuffle_camera-2"] = 2 + + cfg["skeleton"] = [["bodypart1", "bodypart2"], ["objectA", "bodypart3"]] + deeplabcut.auxiliaryfunctions.write_config_3d(path_config_file, cfg) + 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 + date = dt.today() + month = date.strftime("%B") + day = date.day + d = str(month[0:3]+str(day)) + date = dt.today().strftime('%Y-%m-%d') + project_name = '{pn}-{exp}-{date}-{triangulate}'.format(pn=task, exp=scorer, date=date,triangulate='3d') + """ + project_name = path_config_file.split(os.sep)[-2] + + os.chdir(os.path.join(project_name, "calibration_images")) + + file_name = os.path.join(basepath, "stereo_example.zip") + with zipfile.ZipFile(file_name) as zf: + zf.extractall() + + # Deleting unnecessary images; the ones whose corners are not detected and .mat files + 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 + cam1_images = glob.glob(os.path.join(cwd, "left*.jpg")) + cam2_images = glob.glob(os.path.join(cwd, "right*.jpg")) + # Sorting images + cam1_images.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) + cam2_images.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) + for idx, name in enumerate(cam1_images): + os.rename( + name, + 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_" + f"{idx + 1:0=2d}" + ".jpg")), + ) + + # Removing some of the images where the corner was not detected + [os.remove(file) for file in glob.glob(os.path.join(cwd, "*06.jpg"))] + [os.remove(file) for file in glob.glob(os.path.join(cwd, "*01.jpg"))] + + print("CALIBRATING THE CAMERAS") + deeplabcut.calibrate_cameras(path_config_file, calibrate=True) + + print("CHECKING FOR UNDISTORTION") + deeplabcut.check_undistortion(path_config_file) + + 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.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, video_extensions=".avi") + + # output_path = [os.path.join(basepath,folder)] + # deeplabcut.create_labeled_video_3d(path_config_file,output_path,start=5,end=10) + + print("ALL DONE!!! - default 3D cases are functional.") diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index cd1f21a25e..01e033ee65 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -1,5 +1,14 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# """ Created on Tue Oct 2 13:56:11 2018 @author: alex @@ -28,23 +37,19 @@ 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 os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd import numpy as np +import pandas as pd +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" -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 @@ -96,7 +101,7 @@ videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -108,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 1e78021bbe..18342cb347 100644 --- a/examples/testscript_mobilenets.py +++ b/examples/testscript_mobilenets.py @@ -1,27 +1,40 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# """ Created on Tue Oct 2 13:56:11 2018 @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) It produces nothing of interest scientifically. """ + import os -import deeplabcut + +os.environ["DLClight"] = "True" 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 -): +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"], @@ -60,7 +73,7 @@ def Cuttrainingschedule( ) print("CHANGING training parameters to end quickly!") - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) + deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) return @@ -68,25 +81,19 @@ def Cuttrainingschedule( task = "TEST-multipleNets" # 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")) + 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") - deeplabcut.auxiliaryfunctions.attempttomakefolder(dfolder) + deeplabcut.auxiliaryfunctions.attempt_to_make_folder(dfolder) # dfolder=None 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 @@ -131,7 +138,7 @@ def Cuttrainingschedule( videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -141,9 +148,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 @@ -152,9 +157,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, @@ -176,7 +179,7 @@ def Cuttrainingschedule( newvideo = deeplabcut.ShortenVideo( video[0], start="00:00:00", - stop="00:00:00.4", + stop="00:00:01", outsuffix="short", outpath=os.path.join(cfg["project_path"], "videos"), ) @@ -188,7 +191,7 @@ def Cuttrainingschedule( shuffle=shuffle, save_as_csv=True, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("CREATE VIDEO") @@ -197,7 +200,7 @@ def Cuttrainingschedule( [newvideo], shuffle=shuffle, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("Making plots") @@ -206,7 +209,7 @@ def Cuttrainingschedule( [newvideo], shuffle=shuffle, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) print("EXTRACT OUTLIERS") @@ -218,7 +221,7 @@ def Cuttrainingschedule( epsilon=0, automatic=True, destfolder=dfolder, - videotype="avi", + video_extensions="avi", ) file = os.path.join( cfg["project_path"], @@ -230,9 +233,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( @@ -249,7 +250,7 @@ def Cuttrainingschedule( vname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -258,17 +259,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 deleted file mode 100644 index 66e51e14e1..0000000000 --- a/examples/testscript_multianimal.py +++ /dev/null @@ -1,183 +0,0 @@ -import os -import deeplabcut -import numpy as np -import pandas as pd -from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions - - -if __name__ == "__main__": - TASK = "multi_mouse" - SCORER = "dlc_team" - NUM_FRAMES = 5 - TRAIN_SIZE = 0.8 - NET = "dlcrnet_ms5" - # NET = "efficientnet-b0" - N_ITER = 5 - - basepath = os.path.dirname(os.path.realpath(__file__)) - video = "m3v1mp4" - 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 - ) - print("Project created.") - - print("Editing config...") - cfg = auxiliaryfunctions.edit_config( - config_path, {"numframes2pick": NUM_FRAMES, "TrainingFraction": [TRAIN_SIZE]} - ) - print("Config edited.") - - print("Extracting frames...") - deeplabcut.extract_frames(config_path, mode="automatic", userfeedback=False) - print("Frames extracted.") - - print("Creating artificial data...") - rel_folder = os.path.join("labeled-data", os.path.splitext(video)[0]) - image_folder = os.path.join(cfg["project_path"], rel_folder) - n_animals = len(cfg["individuals"]) - ( - animals, - 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) - 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 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)) + 100, 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"), "df_with_missing", format="table", mode="w" - ) - print("Artificial data created.") - - print("Cropping and exchanging") - deeplabcut.cropimagesandlabels(config_path, userfeedback=False) - - print("Checking labels...") - deeplabcut.check_labels(config_path, draw_skeleton=False) - print("Labels checked.") - - print("Creating train dataset...") - deeplabcut.create_multianimaltraining_dataset(config_path, net_type=NET) - print("Train dataset created.") - - print("Editing pose config...") - model_folder = auxiliaryfunctions.GetModelFolder( - TRAIN_SIZE, 1, cfg, cfg["project_path"] - ) - pose_config_path = os.path.join(model_folder, "train/pose_cfg.yaml") - edits = { - "global_scale": 0.5, - "batch_size": 1, - "save_iters": N_ITER, - "display_iters": N_ITER // 2, - # "multi_step": [[0.001, N_ITER]], - } - deeplabcut.auxiliaryfunctions.edit_config(pose_config_path, edits) - print("Pose config edited.") - - print("Training network...") - deeplabcut.train_network(config_path, maxiters=N_ITER) - print("Network trained.") - - print("Evaluating network...") - deeplabcut.evaluate_network(config_path, plotting=True) - - print("Network evaluated....") - - print("Extracting maps...") - deeplabcut.extract_save_all_maps(config_path, Indices=[0, 1, 2]) - - new_video_path = deeplabcut.ShortenVideo( - video_path, - start="00:00:00", - stop="00:00:01", - outsuffix="short", - outpath=os.path.join(cfg["project_path"], "videos"), - ) - - print("Analyzing video...") - deeplabcut.analyze_videos(config_path, [new_video_path], "mp4", robust_nframes=True,allow_growth=True) - - print("Video analyzed.") - - print("Create video with all detections...") - scorer, _ = auxiliaryfunctions.GetScorerName(cfg, 1, TRAIN_SIZE) - deeplabcut.create_video_with_all_detections( - config_path, [new_video_path], shuffle=1, displayedbodyparts=["bodypart1"] - ) - print("Video created.") - - print("Convert detections to tracklets...") - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method="box" - ) - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method="ellipse" - ) - print("Tracklets created...") - - pickle_file = os.path.join( - os.path.dirname(basepath), "tests", "data", "trimouse_tracklets.pickle" - ) - deeplabcut.stitch_tracklets( - config_path, - pickle_file, - output_name=os.path.splitext(new_video_path)[0] + scorer + "_el.h5", - ) - - print("Plotting trajectories...") - deeplabcut.plot_trajectories( - config_path, [new_video_path], "mp4", track_method="ellipse" - ) - print("Trajectory plotted.") - - print("Creating labeled video...") - deeplabcut.create_labeled_video( - config_path, - [new_video_path], - "mp4", - save_frames=False, - color_by="individual", - track_method="ellipse", - ) - print("Labeled video created.") - - print("Filtering predictions...") - deeplabcut.filterpredictions( - config_path, [new_video_path], "mp4", track_method="ellipse" - ) - print("Predictions filtered.") - """ - print("Extracting outlier frames...") - deeplabcut.extract_outlier_frames( - config_path, [new_video_path], "mp4", automatic=True, track_method="ellipse" - ) - print("Outlier frames extracted.") - """ - print("ALL DONE!!! - default multianimal cases are functional.") diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 7ee843b070..8faf1c3a5d 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -1,7 +1,15 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Mon Nov 5 18:06:13 2018 +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Created on Mon Nov 5 18:06:13 2018. @author: alex @@ -17,17 +25,17 @@ 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 deeplabcut + import os +import deeplabcut 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 @@ -35,9 +43,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) @@ -48,12 +54,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 ebc5161667..6e88425d40 100644 --- a/examples/testscript_openfielddata_augmentationcomparison.py +++ b/examples/testscript_openfielddata_augmentationcomparison.py @@ -1,178 +1,61 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" - -This is a test script to compare the loaders. tensorpack allows much more choices for augmentation. The parameters -can be set in pose_dataset_tensorpack.py and of course specifically in each pose_config.yaml file before training. In fact, -pose_dataset_tensorpack.py will fall back to default parameters if they are not defined in pose_config.yaml and one is -using dataset_type:'tensorpack' - -This script creates one identical split for the openfield test dataset and trains it with the -standard loader and the tensorpack loader for k iterations in DLC 2.0 docker with TF 1.8 on a NVIDIA GTX 1080Ti. - -My results were (Run with DLC 2.0.9 in Sept 2019) - -**With standard loader:** - -Training iterations: %Training dataset Shuffle number Train error(px) Test error(px) p-cutoff used Train error with p-cutoff Test error with p-cutoff -10000 80 2 2.64 3.11 0.4 2.64 3.11 -20000 80 2 2.26 2.72 0.4 2.26 2.72 -30000 80 2 1.71 2.28 0.4 1.71 2.28 -40000 80 2 1.88 2.61 0.4 1.88 2.61 -50000 80 2 1.86 2.32 0.4 1.86 2.32 -60000 80 2 1.92 2.42 0.4 1.92 2.42 -70000 80 2 2.38 3.04 0.4 2.38 3.04 -80000 80 2 1.55 2.34 0.4 1.55 2.34 -90000 80 2 1.5 2.27 0.4 1.5 2.27 -100000 80 2 1.52 2.34 0.4 1.52 2.34 - - -**With tensorpack loader:** - -Training iterations: %Training dataset Shuffle number Train error(px) Test error(px) p-cutoff used Train error with p-cutoff Test error with p-cutoff -10000 80 3 2.35 2.91 0.4 2.35 2.91 -20000 80 3 3.28 3.51 0.4 3.28 3.51 -30000 80 3 1.57 2.24 0.4 1.57 2.24 -40000 80 3 3.54 4.17 0.4 3.54 4.17 -50000 80 3 1.76 2.74 0.4 1.76 2.74 -60000 80 3 2.85 3.39 0.4 2.85 3.39 -70000 80 3 3.88 4.71 0.4 3.88 4.71 -80000 80 3 1.2 2.06 0.4 1.2 2.06 -90000 80 3 2.2 3.07 0.4 2.2 3.07 -100000 80 3 1.06 1.96 0.4 1.06 1.96 - - -For details on TensorPack check out: - -A Neural Net Training Interface on TensorFlow, with focus on speed + flexibility -https://github.com/tensorpack/tensorpack - -My results were (Run with DLC 2.2b5 in May 2020) for 20k iterations - -Imagaug augmentation: - -Results for 20000 training iterations: 95 1 train error: 3.25 pixels. Test error: 4.98 pixels. -With pcutoff of 0.4 train error: 3.25 pixels. Test error: 4.98 pixels - -Default augmentation: - -Results for 20000 training iterations: 95 2 train error: 2.5 pixels. Test error: 4.08 pixels. -With pcutoff of 0.4 train error: 2.5 pixels. Test error: 4.08 pixels - -Tensorpack augmentation: - -Results for 20000 training iterations: 95 3 train error: 3.06 pixels. Test error: 4.78 pixels. -With pcutoff of 0.4 train error: 3.06 pixels. Test error: 4.78 pixels - -My results were (Run with DLC *2.2b7* in July 2020) for 20k iterations - -Attention: default changed! - -***Default = Imagaug**** augmentation: - -Done and results stored for snapshot: snapshot-20000 -Results for 20000 training iterations: 95 1 train error: 2.93 pixels. Test error: 3.09 pixels. -With pcutoff of 0.4 train error: 2.93 pixels. Test error: 3.09 pixels - -Scalecrop (was = default) augmentation: - -Done and results stored for snapshot: snapshot-20000 -Results for 20000 training iterations: 95 2 train error: 2.5 pixels. Test error: 2.57 pixels. -With pcutoff of 0.4 train error: 2.5 pixels. Test error: 2.57 pixels - -Tensorpack augmentation: - -Done and results stored for snapshot: snapshot-20000 -Results for 20000 training iterations: 95 3 train error: 3.1 pixels. Test error: 3.29 pixels. -With pcutoff of 0.4 train error: 3.1 pixels. Test error: 3.29 pixels +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""This is a test script to compare the loaders and models. -My results were (Run with DLC *2.2b7* on August 1st 2020) for 10k iterations +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) -Imgaug: -Results for 10000 training iterations: 95 1 train error: 3.78 pixels. Test error: 3.89 pixels. -With pcutoff of 0.4 train error: 3.78 pixels. Test error: 3.89 pixels +My results were (Run with DLC *2.2.0.4* in Jan 6 2022) for 50 k iterations -Scalecrop: -Done and results stored for snapshot: snapshot-10000 -Results for 10000 training iterations: 95 2 train error: 2.81 pixels. Test error: 2.46 pixels. -With pcutoff of 0.4 train error: 2.81 pixels. Test error: 2.46 pixels +DLC_mobnet_35_openfieldOct30shuffle0_50000 and Imgaug with # of training iterations: 50000 +Results for 50000 training iterations: 95 0 train error: 3.06 pixels. Test error: 3.44 pixels. +With pcutoff of 0.4 train error: 3.06 pixels. Test error: 3.44 pixel -Tensorpack: -Done and results stored for snapshot: snapshot-10000 -Results for 10000 training iterations: 95 3 train error: 3.76 pixels. Test error: 3.98 pixels. -With pcutoff of 0.4 train error: 3.76 pixels. Test error: 3.98 pixels +DLC_mobnet_35_openfieldOct30shuffle1_50000 and scalecrop with # of training iterations: 50000 +Results for 50000 training iterations: 95 1 train error: 2.44 pixels. Test error: 3.84 pixels. +With pcutoff of 0.4 train error: 2.44 pixels. Test error: 3.84 pixels +DLC_mobnet_35_openfieldOct30shuffle2_50000 and tensorpack with # of training iterations: 50000 +Results for 50000 training iterations: 95 2 train error: 2.41 pixels. Test error: 3.04 pixels. +With pcutoff of 0.4 train error: 2.41 pixels. Test error: 3.04 pixels -My results were (Run with DLC *2.2b8* on Sept 7 2020) for 10k iterations +DLC_resnet50_openfieldOct30shuffle3_50000 and Imgaug with # of training iterations: 50000 +Results for 50000 training iterations: 95 3 train error: 2.69 pixels. Test error: 2.97 pixels. +With pcutoff of 0.4 train error: 2.69 pixels. Test error: 2.97 pixels -Imgaug: -Results for 10000 training iterations: 95 1 train error: 2.63 pixels. Test error: 3.88 pixels. -With pcutoff of 0.4 train error: 2.63 pixels. Test error: 3.88 pixels +DLC_resnet50_openfieldOct30shuffle4_50000 and scalecrop with # of training iterations: 50000 +Results for 50000 training iterations: 95 4 train error: 2.0 pixels. Test error: 2.69 pixels. +With pcutoff of 0.4 train error: 2.0 pixels. Test error: 2.69 pixels -Scalecrop: -Results for 10000 training iterations: 95 2 train error: 3.08 pixels. Test error: 4.02 pixels. -With pcutoff of 0.4 train error: 3.08 pixels. Test error: 4.02 pixels +DLC_resnet50_openfieldOct30shuffle5_50000 and tensorpack with # of training iterations: 50000 +Results for 50000 training iterations: 95 5 train error: 1.96 pixels. Test error: 2.65 pixels. +With pcutoff of 0.4 train error: 1.96 pixels. Test error: 2.65 pixels -Tensorpack: -Results for 10000 training iterations: 95 3 train error: 2.9 pixels. Test error: 3.31 pixels. -With pcutoff of 0.4 train error: 2.9 pixels. Test error: 3.31 pixels +DLC_effnet_b3_openfieldOct30shuffle6_50000 with Imgaug with # of training iterations: 50000 +Results for 50000 training iterations: 95 6 train error: 2.63 pixels. Test error: 2.65 pixels. +With pcutoff of 0.4 train error: 2.63 pixels. Test error: 2.65 pixels -My results were (Run with DLC *2.1.9* in Jan 2021) for 10 k iterations +effnet with tensorpack and scalecrop didn't converge. -**ResNet50 -Imgaug: -Results for 100000 training iterations: 95 1 train error: 2.13 pixels. Test error: 2.22 pixels. -With pcutoff of 0.4 train error: 2.13 pixels. Test error: 2.22 pixels - -Scalecrop: -Results for 100000 training iterations: 95 2 train error: 1.47 pixels. Test error: 1.77 pixels. -With pcutoff of 0.4 train error: 1.47 pixels. Test error: 1.77 pixels - -Tensorpack: -Results for 100000 training iterations: 95 3 train error: 2.09 pixels. Test error: 2.36 pixels. -With pcutoff of 0.4 train error: 2.09 pixels. Test error: 2.36 pixels - -**EffNet-b3 -Imgaug: -Results for 100000 training iterations: 95 4 train error: 2.39 pixels. Test error: 2.57 pixels. -With pcutoff of 0.4 train error: 2.39 pixels. Test error: 2.57 pixels - -Scalecrop: -Results for 100000 training iterations: 95 5 train error: 2.26 pixels. Test error: 2.24 pixels. -With pcutoff of 0.4 train error: 2.26 pixels. Test error: 2.24 pixels - -Tensorpack: -Results for 100000 training iterations: 95 6 train error: 1.65 pixels. Test error: 2.24 pixels. -With pcutoff of 0.4 train error: 1.65 pixels. Test error: 2.24 pixels 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) - -My results were (Run with DLC *2.10.4* in Apr 2021) for 100 k iterations - -ResNet50: -Imgaug: (includes new default contrast augmentation!) -Done and results stored for snapshot: snapshot-100000 -Results for 100000 training iterations: 95 1 train error: 1.77 pixels. Test error: 2.24 pixels. -With pcutoff of 0.4 train error: 1.77 pixels. Test error: 2.24 pixels - -Scalecrop: -Done and results stored for snapshot: snapshot-100000 -Results for 100000 training iterations: 95 2 train error: 2.11 pixels. Test error: 3.26 pixels. -With pcutoff of 0.4 train error: 2.11 pixels. Test error: 3.26 pixels - -TensorPack: -Results for 100000 training iterations: 95 3 train error: 1.35 pixels. Test error: 2.3 pixels. -With pcutoff of 0.4 train error: 1.35 pixels. Test error: 2.3 pixels - """ - 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") @@ -181,36 +64,38 @@ maxiters = 50000 saveiters = 10000 displayiters = 500 -Shuffles = 1 + np.arange(6) -deeplabcut.load_demo_data(path_config_file) -## Create one split and make Shuffle 2 and 3 have the same split. +deeplabcut.load_demo_data(path_config_file, createtrainingset=False) +## Create one identical splits for 3 networks and 3 augmentations + ###Note that the new function in DLC 2.1 simplifies network/augmentation comparisons greatly: -deeplabcut.create_training_model_comparison( +Shuffles = deeplabcut.create_training_model_comparison( path_config_file, num_shuffles=1, - net_types=["resnet_50", "efficientnet-b3"], + net_types=["mobilenet_v2_0.35", "resnet_50", "efficientnet-b3"], augmenter_types=["imgaug", "scalecrop", "tensorpack"], ) +for idx, shuffle in enumerate(Shuffles): + posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) -for shuffle in Shuffles: - - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) - - edits = {"decay_steps": maxiters, "lr_init": 0.0005} # * 8} # for EfficientNet - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) - - if shuffle % 3 == 1: # imgaug + # Setting specific parameters for training + if idx % 3 == 0: # imgaug edits = {"rotation": 180, "motion_blur": True} DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) - - elif shuffle % 3 == 0: # Tensorpack: + elif idx % 3 == 2: # Tensorpack edits = {"rotation": 180, "noise_sigma": 0.01} DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) + if idx > 5: # EfficientNet + print(posefile, "changing now!!") + edits = { + "decay_steps": maxiters, + "lr_init": 0.0005, + } + DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) + +for shuffle in Shuffles: print("TRAIN NETWORK", shuffle) deeplabcut.train_network( path_config_file, @@ -221,18 +106,15 @@ max_snapshots_to_keep=11, ) -print("EVALUATE") -deeplabcut.evaluate_network(path_config_file, Shuffles=Shuffles, plotting=True) - -for shuffles in Shuffle: 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) print("Create Labeled Video and plot") deeplabcut.create_labeled_video(path_config_file, [videofile_path], shuffle=shuffle) deeplabcut.plot_trajectories(path_config_file, [videofile_path], shuffle=shuffle) + +print("EVALUATE") +deeplabcut.evaluate_network(path_config_file, Shuffles=Shuffles, plotting=False) diff --git a/examples/testscript_openfielddata_netcomparison.py b/examples/testscript_openfielddata_netcomparison.py deleted file mode 100644 index e09353f249..0000000000 --- a/examples/testscript_openfielddata_netcomparison.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -This is a test script to compare the networks. On Jan 3rd 2020: - -Jan 2020: -MobileNetV2 0.35 -Results for 10000 training iterations: 95 1 train error: 5.79 pixels. Test error: 5.63 pixels. -With pcutoff of 0.4 train error: 5.79 pixels. Test error: 5.63 pixels - -ResNet 50 -Results for 10000 training iterations: 95 2 train error: 3.61 pixels. Test error: 3.7 pixels. -With pcutoff of 0.4 train error: 3.61 pixels. Test error: 3.7 pixels - -EffNet-b3 -Results for 10000 training iterations: 95 3 train error: 6.86 pixels. Test error: 6.63 pixels. -With pcutoff of 0.4 train error: 6.86 pixels. Test error: 6.63 pixels - -Note: Not too good on video either! - -TODO: Note we should still optimize the MobNet & EffNet learning rates for this dataset (also training is pretty short!) -TODO: change to frozen backbone! -""" - - -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") -cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) -maxiters = 10000 - -deeplabcut.load_demo_data(path_config_file) - -## Create one split and make Shuffle 2 and 3 have the same split. -###Note that the new function in DLC 2.1 simplifies network/augmentation comparisons greatly: -deeplabcut.create_training_model_comparison( - path_config_file, - num_shuffles=1, - net_types=["mobilenet_v2_0.35", "resnet_50", "efficientnet-b3"], - augmenter_types=["imgaug"], -) - -freezeencoder = False # True -for shuffle in 1 + np.arange(3): - - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) - - # for EfficientNet - edits = { - "decay_steps": maxiters, - "lr_init": 0.0005 * 12, - "freezeencoder": freezeencoder, - } - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) - # imgaug - edits = {"rotation": 180, "motion_blur": True, "freezeencoder": freezeencoder} - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) - - print("TRAIN NETWORK", shuffle) - deeplabcut.train_network( - path_config_file, - shuffle=shuffle, - saveiters=10000, - displayiters=200, - maxiters=maxiters, - max_snapshots_to_keep=11, - ) - - print("EVALUATE") - 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) - - print("Create Labeled Video and plot") - deeplabcut.create_labeled_video(path_config_file, [videofile_path], shuffle=shuffle) - deeplabcut.plot_trajectories(path_config_file, [videofile_path], shuffle=shuffle) diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index 03f5b9e092..3a668c5caa 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -1,42 +1,49 @@ -""" -Testscript human network - -""" -import os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd -import numpy as np +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Testscript human network.""" + +import os + +import deeplabcut Task = "human_dancing" YourName = "teamDLC" +MODEL_NAME = "horse_sideview" # full_human" -basepath = os.path.dirname(os.path.abspath("testscript.py")) +basepath = os.path.dirname(os.path.abspath("testscript_tensorflow_single_animal.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: """ 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( Task, YourName, video, - model="full_human", - videotype="avi", + model=MODEL_NAME, + video_extensions="avi", analyzevideo=True, createlabeledvideo=True, copy_videos=False, + engine=deeplabcut.Engine.TF, ) # must leave copy_videos=True +""" lastvalue = 5 DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(path_train_config) pretrainedDeeperCutweights = DLC_config["init_weights"] @@ -78,7 +85,7 @@ videoname, "CollectedData_" + cfg["scorer"] + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -154,7 +161,7 @@ videoname, "CollectedData_" + cfg["scorer"] + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -169,3 +176,4 @@ # deeplabcut.train_network(configfile,shuffle=1) #>> fails one body part too much! deeplabcut.train_network(configfile, shuffle=1, keepdeconvweights=False) +""" diff --git a/examples/testscript_pytorch_multi_animal.py b/examples/testscript_pytorch_multi_animal.py new file mode 100644 index 0000000000..e4a98d8d47 --- /dev/null +++ b/examples/testscript_pytorch_multi_animal.py @@ -0,0 +1,147 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Testscript for single animal PyTorch projects.""" + +from __future__ import annotations + +from pathlib import Path + +from utils import ( + SyntheticProjectParameters, + cleanup, + create_fake_project, + log_step, + run, +) + +# Enable pandas future mode warnings if DLC_PANDAS_FUTURE env var is set +from deeplabcut.utils.pandas_future_mode import configure_pandas_future_if_enabled + +configure_pandas_future_if_enabled() + +import deeplabcut.utils.auxiliaryfunctions as af # noqa: E402 +from deeplabcut.compat import Engine # noqa: E402 +from deeplabcut.pose_estimation_pytorch.config.utils import is_model_cond_top_down, is_model_top_down # noqa: E402 + + +def main( + net_types: list[str], + params: SyntheticProjectParameters, + epochs: int = 1, + top_down_epochs: int = 1, + detector_epochs: int = 1, + save_epochs: int = 1, + batch_size: int = 1, + detector_batch_size: int = 1, + max_snapshots_to_keep: int = 5, + device: str = "cpu", + logger: dict | None = None, + conditions_shuffle: int = 0, + create_labeled_videos: bool = False, + delete_after_test_run: bool = False, +) -> None: + project_path = Path("synthetic-data-niels-multi-animal").resolve() + config_path = project_path / "config.yaml" + create_fake_project(path=project_path, params=params) + + engine = Engine.PYTORCH + cfg = af.read_config(config_path) + trainset_index = 0 + train_frac = cfg["TrainingFraction"][trainset_index] + try: + for net_type in net_types: + epochs_ = epochs + if is_model_top_down(net_type): + epochs_ = top_down_epochs + try: + pytorch_cfg_updates = { + "train_settings.display_iters": 50, + "train_settings.epochs": epochs_, + "train_settings.batch_size": batch_size, + "train_settings.dataloader_workers": 0, + "runner.device": device, + "runner.snapshots.save_epochs": save_epochs, + "runner.snapshots.max_snapshots": max_snapshots_to_keep, + "logger": logger, + } + + # Only add detector config updates for top-down models + if is_model_top_down(net_type): + pytorch_cfg_updates.update( + { + "detector.train_settings.display_iters": 1, + "detector.train_settings.epochs": detector_epochs, + "detector.train_settings.batch_size": detector_batch_size, + "detector.train_settings.dataloader_workers": 0, + "detector.runner.snapshots.save_epochs": save_epochs, + "detector.runner.snapshots.max_snapshots": max_snapshots_to_keep, + } + ) + + run( + config_path=config_path, + train_fraction=train_frac, + trainset_index=trainset_index, + net_type=net_type, + videos=[str(project_path / "videos" / "video.mp4")], + device=device, + engine=engine, + pytorch_cfg_updates=pytorch_cfg_updates, + create_labeled_videos=create_labeled_videos, + ctd_conditions=(conditions_shuffle, -1) if is_model_cond_top_down(net_type) else None, + ) + except Exception as err: + log_step(f"FAILED TO RUN {net_type}") + log_step(str(err)) + log_step("Continuing to next model") + raise err + + finally: + if delete_after_test_run: + cleanup(project_path) + + +if __name__ == "__main__": + wandb_logger = { + "type": "WandbLogger", + "project_name": "testscript-dev", + "run_name": "test-logging", + } + net_types = [ + "top_down_resnet_50", + "resnet_50", + "dekr_w32", + "rtmpose_m", + "ctd_coam_w32", + ] + main( + net_types=net_types, + params=SyntheticProjectParameters( + multianimal=True, + num_bodyparts=4, + num_individuals=3, + num_unique=0, + num_frames=25, + frame_shape=(256, 256), + ), + batch_size=2, + detector_batch_size=2, + epochs=8, + top_down_epochs=2, + detector_epochs=10, + save_epochs=4, + max_snapshots_to_keep=2, + device="cpu", # "cpu", "cuda:0", "mps" + logger=None, + conditions_shuffle=net_types.index("resnet_50") + 1, # shuffles start at index 1 + create_labeled_videos=True, + delete_after_test_run=True, + ) diff --git a/examples/testscript_pytorch_single_animal.py b/examples/testscript_pytorch_single_animal.py new file mode 100644 index 0000000000..19f3634fe3 --- /dev/null +++ b/examples/testscript_pytorch_single_animal.py @@ -0,0 +1,115 @@ +"""Testscript for single animal PyTorch projects.""" + +from __future__ import annotations + +from pathlib import Path + +from utils import ( + SyntheticProjectParameters, + cleanup, + copy_project_for_test, + create_fake_project, + log_step, + run, +) + +# Enable pandas future mode warnings if DLC_PANDAS_FUTURE env var is set +from deeplabcut.utils.pandas_future_mode import configure_pandas_future_if_enabled + +configure_pandas_future_if_enabled() + +import deeplabcut.utils.auxiliaryfunctions as af # noqa: E402 +from deeplabcut.compat import Engine # noqa: E402 + + +def main( + synthetic_data: bool, + net_types: list[str], + epochs: int = 1, + save_epochs: int = 1, + max_snapshots_to_keep: int = 5, + batch_size: int = 1, + device: str = "cpu", + logger: dict | None = None, + 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() + videos = [str(project_path / "videos" / "video.mp4")] + create_fake_project(path=project_path, params=synthetic_data_params) + + else: + project_path = copy_project_for_test() + videos = [str(project_path / "videos" / "m3v1mp4.mp4")] + + config_path = project_path / "config.yaml" + cfg = af.read_config(config_path) + trainset_index = 0 + train_frac = cfg["TrainingFraction"][trainset_index] + try: + for net_type in net_types: + try: + run( + config_path=config_path, + train_fraction=train_frac, + trainset_index=trainset_index, + net_type=net_type, + videos=videos, + device=device, + engine=engine, + pytorch_cfg_updates={ + "train_settings.display_iters": 50, + "train_settings.epochs": epochs, + "train_settings.batch_size": batch_size, + "runner.device": device, + "runner.snapshots.save_epochs": save_epochs, + "runner.snapshots.max_snapshots": max_snapshots_to_keep, + "logger": logger, + }, + create_labeled_videos=create_labeled_videos, + ) + + except Exception as err: + log_step(f"FAILED TO RUN {net_type}") + log_step(str(err)) + log_step("Continuing to next model") + raise err + finally: + if delete_after_test_run: + cleanup(project_path) + + +if __name__ == "__main__": + wandb_logger = { + "type": "WandbLogger", + "project_name": "testscript-dev", + "run_name": "test-logging", + } + main( + synthetic_data=True, + net_types=["cspnext_m", "resnet_50", "hrnet_w32"], + batch_size=4, + epochs=8, + save_epochs=2, + max_snapshots_to_keep=2, + device="cpu", # "cpu", "cuda:0", "mps" + logger=None, + synthetic_data_params=SyntheticProjectParameters( + multianimal=False, + num_bodyparts=4, + num_individuals=1, + num_unique=0, + num_frames=12, + frame_shape=(128, 128), + ), + create_labeled_videos=True, + delete_after_test_run=True, + ) diff --git a/examples/testscript_superanimal_adaptation.py b/examples/testscript_superanimal_adaptation.py new file mode 100644 index 0000000000..42d6e2fba9 --- /dev/null +++ b/examples/testscript_superanimal_adaptation.py @@ -0,0 +1,43 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Test script for super animal adaptation.""" + +import os + +import deeplabcut + +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 = deeplabcut.ShortenVideo( + video, + start="00:00:00", + stop="00:00:01", + outsuffix="short", + ) + + print("adaptation training for superanimal_topviewmouse") + + superanimal_name = "superanimal_topviewmouse" + video_extensions = ".mp4" + scale_list = [200, 300, 400] + deeplabcut.video_inference_superanimal( + [video], + superanimal_name, + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".mp4", + video_adapt=True, + scale_list=scale_list, + pcutoff=0.1, + adapt_iterations=50, + ) diff --git a/examples/testscript_superanimal_create_pretrained_project.py b/examples/testscript_superanimal_create_pretrained_project.py new file mode 100644 index 0000000000..b697a3e285 --- /dev/null +++ b/examples/testscript_superanimal_create_pretrained_project.py @@ -0,0 +1,38 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Testscript for creating a pretrained project from a super animal model.""" + +import glob +import shutil +from pathlib import Path + +import deeplabcut + +if __name__ == "__main__": + superanimal_name = "superanimal_quadruped" + working_dir = Path(__file__).resolve().parent + video_dir = working_dir / "openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4" + project_name = "pretrained" + + deeplabcut.create_pretrained_project( + project_name, + "max", + [str(video_dir)], + engine=deeplabcut.Engine.PYTORCH, + ) + + dirs_to_delete = glob.glob(f"{working_dir}/{project_name}*") + + # Delete directories + for directory in dirs_to_delete: + shutil.rmtree(directory) + + print("Test passed!") diff --git a/examples/testscript_superanimal_inference.py b/examples/testscript_superanimal_inference.py new file mode 100644 index 0000000000..4db84b2b91 --- /dev/null +++ b/examples/testscript_superanimal_inference.py @@ -0,0 +1,43 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Testscript for super animal inference.""" + +import os + +import deeplabcut + +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")] + + print("testing superanimal_topviewmouse") + superanimal_name = "superanimal_topviewmouse" + scale_list = [200, 300, 400] + deeplabcut.video_inference_superanimal( + video, + superanimal_name, + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".avi", + scale_list=scale_list, + ) + + print("testing superanimal_quadruped") + superanimal_name = "superanimal_quadruped" + deeplabcut.video_inference_superanimal( + video, + superanimal_name, + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + video_extensions=".avi", + scale_list=scale_list, + ) diff --git a/examples/testscript_superanimal_transfer_learning.py b/examples/testscript_superanimal_transfer_learning.py new file mode 100644 index 0000000000..cced5b26e6 --- /dev/null +++ b/examples/testscript_superanimal_transfer_learning.py @@ -0,0 +1,44 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Test script for super animal adaptation.""" + +import os + +import deeplabcut +from deeplabcut.modelzoo.weight_initialization import build_weight_init + +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") + model_name = "hrnet_w32" + detector_name = "fasterrcnn_resnet50_fpn_v2" + + weight_init = build_weight_init( + cfg=config_path, + super_animal=superanimal_name, + model_name=model_name, + detector_name=detector_name, + with_decoder=False, + ) + deeplabcut.create_training_dataset( + config_path, + weight_init=weight_init, + net_type=model_name, + ) + + deeplabcut.train_network( + config_path, + epochs=1, + superanimal_name=superanimal_name, + superanimal_transfer_learning=True, + ) diff --git a/examples/testscript_tensorflow_multi_animal.py b/examples/testscript_tensorflow_multi_animal.py new file mode 100644 index 0000000000..c90ec120e7 --- /dev/null +++ b/examples/testscript_tensorflow_multi_animal.py @@ -0,0 +1,326 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +import pickle +import random +from pathlib import Path + +import matplotlib +import numpy as np +import pandas as pd + +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows + +# Enable pandas future mode warnings if DLC_PANDAS_FUTURE env var is set +from deeplabcut.utils.pandas_future_mode import configure_pandas_future_if_enabled + +configure_pandas_future_if_enabled() + +import deeplabcut # noqa: E402 +from deeplabcut.core.engine import Engine # noqa: E402 +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions # noqa: E402 +from deeplabcut.utils.auxfun_videos import VideoReader # noqa: E402 + +MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0"] + + +N_ITER = 5 +TESTTRACKER = "ellipse" + +USE_SHELVE = False # random.choice([True, False]) + +if __name__ == "__main__": + TASK = "multi_mouse" + SCORER = "dlc_team" + NUM_FRAMES = 5 + TRAIN_SIZE = 0.8 + ENGINE = Engine.TF + + # NET = "dlcr101_ms5" + NET = "dlcrnet_ms5" + + # Always test a different model from list above + NET = random.choice(MODELS) + + basepath = os.path.dirname(os.path.realpath(__file__)) + DESTFOLDER = basepath + + video = "m3v1mp4" + 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) + + print("Project created.") + + print("Editing config...") + cfg = auxiliaryfunctions.edit_config( + config_path, + { + "numframes2pick": NUM_FRAMES, + "TrainingFraction": [TRAIN_SIZE], + "identity": True, + "uniquebodyparts": ["corner1", "corner2"], + }, + ) + print("Config edited.") + + print("Extracting frames...") + deeplabcut.extract_frames(config_path, mode="automatic", userfeedback=False) + print("Frames extracted.") + + print("Creating artificial data...") + rel_folder = os.path.join("labeled-data", os.path.splitext(video)[0]) + image_folder = os.path.join(cfg["project_path"], rel_folder) + n_animals = len(cfg["individuals"]) + ( + animals, + 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) + 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) + 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)) + 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", + ) + print("Artificial data created.") + + print("Checking labels...") + deeplabcut.check_labels(config_path, draw_skeleton=False) + print("Labels checked.") + + print("Creating train dataset...") + deeplabcut.create_multianimaltraining_dataset( + config_path, + net_type=NET, + crop_size=(200, 200), + engine=ENGINE, + ) + print("Train dataset created.") + + # Check the training image paths are correctly stored as arrays of strings + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) + datafile, _ = auxiliaryfunctions.get_data_and_metadata_filenames( + trainingsetfolder, + 0.8, + 1, + cfg, + ) + datafile = datafile.with_suffix(".pickle") + with open(os.path.join(cfg["project_path"], datafile), "rb") as f: + pickledata = pickle.load(f) + num_images = len(pickledata) + assert all(len(pickledata[i]["image"]) == 3 for i in range(num_images)) + + print("Editing pose config...") + model_folder = auxiliaryfunctions.get_model_folder( + TRAIN_SIZE, 1, cfg, engine=ENGINE, modelprefix=cfg["project_path"] + ) + pose_config_path = os.path.join(model_folder, "train", "pose_cfg.yaml") + edits = { + "global_scale": 0.5, + "batch_size": 1, + "save_iters": N_ITER, + "display_iters": N_ITER // 2, + "crop_size": [200, 200], + # "multi_step": [[0.001, N_ITER]], + } + deeplabcut.auxiliaryfunctions.edit_config(pose_config_path, edits) + print("Pose config edited.") + + print("Training network...") + deeplabcut.train_network(config_path, maxiters=N_ITER) + print("Network trained.") + + print("Evaluating network...") + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) + + print("Network evaluated....") + + print("Extracting maps...") + deeplabcut.extract_save_all_maps(config_path, Indices=[0, 1, 2]) + + new_video_path = deeplabcut.ShortenVideo( + video_path, + start="00:00:00", + stop="00:00:01", + outsuffix="short", + outpath=os.path.join(cfg["project_path"], "videos"), + ) + + print("Analyzing video...") + try: + deeplabcut.analyze_videos( + config_path, + [new_video_path], + "mp4", + robust_nframes=True, + allow_growth=True, + use_shelve=USE_SHELVE, + ) + print("Video analyzed.") + except ValueError: + pass + + print("Create video with all detections...") + scorer, _ = auxiliaryfunctions.get_scorer_name(cfg, 1, TRAIN_SIZE) + + deeplabcut.create_video_with_all_detections( + config_path, [new_video_path], shuffle=1, displayedbodyparts=["bodypart1"] + ) + + print("Video created.") + + print("Convert detections to tracklets...") + 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: + deeplabcut.stitch_tracklets( + config_path, + [new_video_path], + "mp4", + output_name=h5path, + track_method=TESTTRACKER, + ) + except ValueError: + # Sometimes tracks cannot be reconstructed as test data are randomly + # created; when this happens, we generate a fake h5 data file. + 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 bp in bodyparts_single for _ in range(3)] + columns = pd.MultiIndex.from_arrays( + [scorer, individuals, bodyparts, coords], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + vid = VideoReader(new_video_path) + fake_data = np.ones((len(vid), columns.shape[0])) + df = pd.DataFrame(fake_data, columns=columns) + df.to_hdf(h5path, key="data") + + print("Plotting trajectories...") + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) + print("Trajectory plotted.") + + print("Creating labeled video...") + deeplabcut.create_labeled_video( + config_path, + [new_video_path], + "mp4", + save_frames=False, + color_by="individual", + track_method=TESTTRACKER, + ) + print("Labeled video created.") + + print("Filtering predictions...") + 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) + print("Outlier frames extracted.") + + vname = Path(new_video_path).stem + + file = os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "machinelabels-iter" + str(cfg["iteration"]) + ".h5", + ) + + print("RELABELING") + DF = pd.read_hdf(file, "df_with_missing") + DLCscorer = np.unique(DF.columns.get_level_values(0))[0] + DF.columns = DF.columns.set_levels([scorer.replace(DLCscorer, SCORER)], level=0) + DF = DF.drop("likelihood", axis=1, level=3) + DF.to_csv( + os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "CollectedData_" + SCORER + ".csv", + ) + ) + DF.to_hdf( + os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "CollectedData_" + SCORER + ".h5", + ), + key="df_with_missing", + ) + + print("MERGING") + deeplabcut.merge_datasets(config_path) # iteration + 1 + + print("CREATING TRAININGSET updated training set") + deeplabcut.create_training_dataset(config_path, net_type=NET, engine=ENGINE) + + print("Training network...") + deeplabcut.train_network(config_path, maxiters=N_ITER) + print("Network trained.") + + print("Evaluating network...") + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) + + print("Network evaluated....") + + print("Analyzing video with auto_track....") + deeplabcut.analyze_videos( + config_path, + [new_video_path], + save_as_csv=True, + destfolder=DESTFOLDER, + cropping=[0, 50, 0, 50], + allow_growth=True, + use_shelve=USE_SHELVE, + auto_track=False, + ) + + print("Export model...") + deeplabcut.export_model(config_path, shuffle=1, make_tar=False) + + print("Merging datasets...") + trainIndices, testIndices = deeplabcut.mergeandsplit(config_path, trainindex=0, uniform=True) + + print("Creating two identical splits...") + deeplabcut.create_multianimaltraining_dataset( + config_path, + Shuffles=[4, 5], + net_type=NET, + trainIndices=[trainIndices, trainIndices], + testIndices=[testIndices, testIndices], + engine=ENGINE, + ) + + print("ALL DONE!!! - default multianimal cases are functional.") diff --git a/examples/testscript.py b/examples/testscript_tensorflow_single_animal.py similarity index 69% rename from examples/testscript.py rename to examples/testscript_tensorflow_single_animal.py index feaa0d405f..c169ca8387 100644 --- a/examples/testscript.py +++ b/examples/testscript_tensorflow_single_animal.py @@ -1,5 +1,14 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# """ Created on Tue Oct 2 13:56:11 2018 @author: alex @@ -12,39 +21,50 @@ It produces nothing of interest scientifically. """ + import os -import deeplabcut import platform -import subprocess +import random from pathlib import Path +import matplotlib import numpy as np import pandas as pd +import scipy.io as sio + +# Enable pandas future mode warnings if DLC_PANDAS_FUTURE env var is set +from deeplabcut.utils.pandas_future_mode import configure_pandas_future_if_enabled + +configure_pandas_future_if_enabled() + +import deeplabcut # noqa: E402 +from deeplabcut.core.engine import Engine # noqa: E402 +from deeplabcut.utils import auxiliaryfunctions # noqa: E402 + +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows + +USE_SHELVE = random.choice([True, False]) +MODELS = ["resnet_50", "efficientnet-b0"] if __name__ == "__main__": task = "TEST" # Enter the name of your experiment Task scorer = "Alex" # Enter the name of the experimenter/labeler + engine = Engine.TF 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' # video=[os.path.join('/home/alex/Desktop/Data',videoname+'.mp4')] # to test destination folder: - dfolder = basepath + DESTFOLDER = basepath - dfolder = None - net_type = "resnet_50" #'mobilenet_v2_0.35' #'resnet_50' - # net_type = "mobilenet_v2_0.35" - # net_type = "efficientnet-b0" # to -b6 + DESTFOLDER = None + NET = random.choice(MODELS) augmenter_type = "default" # = imgaug!! augmenter_type2 = "scalecrop" @@ -55,12 +75,11 @@ else: augmenter_type3 = "tensorpack" # Does not work on WINDOWS - numiter = 5 + N_ITER = 6 + 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 @@ -75,6 +94,7 @@ print("CREATING-SOME LABELS FOR THE FRAMES") frames = os.listdir(os.path.join(cfg["project_path"], "labeled-data", videoname)) + frames = [fn for fn in frames if fn.endswith(".png")] # As this next step is manual, we update the labels by putting them on the diagonal (fixed for all frames) for index, bodypart in enumerate(cfg["bodyparts"]): columnindex = pd.MultiIndex.from_product( @@ -106,7 +126,7 @@ videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -117,9 +137,27 @@ print("CREATING TRAININGSET") deeplabcut.create_training_dataset( - path_config_file, net_type=net_type, augmenter_type=augmenter_type + path_config_file, + net_type=NET, + augmenter_type=augmenter_type, + engine=engine, ) + # Check the training image paths are correctly stored as arrays of strings + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) + datafile, _ = auxiliaryfunctions.get_data_and_metadata_filenames( + trainingsetfolder, + 0.8, + 1, + cfg, + ) + mlab = sio.loadmat(os.path.join(cfg["project_path"], datafile))["dataset"] + num_images = mlab.shape[1] + for i in range(num_images): + imgpath = mlab[0, i][0][0] + assert len(imgpath) == 3 + assert imgpath.dtype.char == "U" + posefile = os.path.join( cfg["project_path"], "dlc-models/iteration-" @@ -135,18 +173,26 @@ ) DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(posefile) - DLC_config["save_iters"] = numiter + DLC_config["save_iters"] = SAVE_ITER DLC_config["display_iters"] = 2 - DLC_config["multi_step"] = [[0.001, numiter]] print("CHANGING training parameters to end quickly!") deeplabcut.auxiliaryfunctions.write_plainconfig(posefile, DLC_config) print("TRAIN") - deeplabcut.train_network(path_config_file) + deeplabcut.train_network(path_config_file, maxiters=N_ITER) print("EVALUATE") - deeplabcut.evaluate_network(path_config_file, plotting=True) + deeplabcut.evaluate_network( + path_config_file, + plotting=True, + per_keypoint_evaluation=True, + snapshots_to_evaluate=[ + "snapshot-3", + "snapshot-5", + "snapshot-6", + ], # snapshot-5 intentionally missing :) + ) # deeplabcut.evaluate_network(path_config_file,plotting=True,trainingsetindex=33) print("CUT SHORT VIDEO AND ANALYZE (with dynamic cropping!)") @@ -161,10 +207,10 @@ 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 VideoFileClip, VideoClip + from moviepy.editor import VideoClip, VideoFileClip clip = VideoFileClip(video[0]) clip.reader.initialize() @@ -181,22 +227,19 @@ def make_frame(t): path_config_file, [newvideo], save_as_csv=True, - destfolder=dfolder, + destfolder=DESTFOLDER, dynamic=(True, 0.1, 5), ) print("analyze again...") - deeplabcut.analyze_videos( - path_config_file, [newvideo], save_as_csv=True, destfolder=dfolder - ) + deeplabcut.analyze_videos(path_config_file, [newvideo], save_as_csv=True, destfolder=DESTFOLDER) print("CREATE VIDEO") - deeplabcut.create_labeled_video( - path_config_file, [newvideo], destfolder=dfolder, save_frames=True - ) + successful = deeplabcut.create_labeled_video(path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True) + assert all(successful), "Failed to create a labeled video!" print("Making plots") - deeplabcut.plot_trajectories(path_config_file, [newvideo], destfolder=dfolder) + deeplabcut.plot_trajectories(path_config_file, [newvideo], destfolder=DESTFOLDER) print("EXTRACT OUTLIERS") deeplabcut.extract_outlier_frames( @@ -205,7 +248,7 @@ def make_frame(t): outlieralgorithm="jump", epsilon=0, automatic=True, - destfolder=dfolder, + destfolder=DESTFOLDER, ) deeplabcut.extract_outlier_frames( @@ -213,7 +256,7 @@ def make_frame(t): [newvideo], outlieralgorithm="fitting", automatic=True, - destfolder=dfolder, + destfolder=DESTFOLDER, ) file = os.path.join( @@ -226,7 +269,7 @@ def make_frame(t): 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 = DF.columns.set_levels([scorer.replace(DLCscorer, scorer)], level=0) DF = DF.drop("likelihood", axis=1, level=2) DF.to_csv( os.path.join( @@ -243,18 +286,14 @@ def make_frame(t): vname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", - format="table", - mode="w", + key="df_with_missing", ) print("MERGING") deeplabcut.merge_datasets(path_config_file) # iteration + 1 print("CREATING TRAININGSET") - deeplabcut.create_training_dataset( - path_config_file, net_type=net_type, augmenter_type=augmenter_type2 - ) + 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( @@ -271,15 +310,14 @@ def make_frame(t): "train/pose_cfg.yaml", ) DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(posefile) - DLC_config["save_iters"] = numiter + DLC_config["save_iters"] = SAVE_ITER DLC_config["display_iters"] = 1 - DLC_config["multi_step"] = [[0.001, numiter]] print("CHANGING training parameters to end quickly!") deeplabcut.auxiliaryfunctions.write_config(posefile, DLC_config) print("TRAIN") - deeplabcut.train_network(path_config_file) + deeplabcut.train_network(path_config_file, maxiters=N_ITER) try: # you need ffmpeg command line interface # subprocess.call(['ffmpeg','-i',video[0],'-ss','00:00:00','-to','00:00:00.4','-c','copy',newvideo]) @@ -291,11 +329,9 @@ def make_frame(t): outpath=os.path.join(cfg["project_path"], "videos"), ) - except: # if ffmpeg is broken - newvideo2 = os.path.join( - cfg["project_path"], "videos", videoname + "short2.mp4" - ) - from moviepy.editor import VideoFileClip, VideoClip + except Exception: # if ffmpeg is broken + newvideo2 = os.path.join(cfg["project_path"], "videos", videoname + "short2.mp4") + from moviepy.editor import VideoClip, VideoFileClip clip = VideoFileClip(video[0]) clip.reader.initialize() @@ -313,33 +349,32 @@ def make_frame(t): path_config_file, [newvideo2], save_as_csv=True, - destfolder=dfolder, + destfolder=DESTFOLDER, cropping=[0, 50, 0, 50], - allow_growth=True + allow_growth=True, + use_shelve=USE_SHELVE, ) print("Extracting skeleton distances, filter and plot filtered output") - deeplabcut.analyzeskeleton( - path_config_file, [newvideo2], save_as_csv=True, destfolder=dfolder - ) + deeplabcut.analyzeskeleton(path_config_file, [newvideo2], save_as_csv=True, destfolder=DESTFOLDER) deeplabcut.filterpredictions(path_config_file, [newvideo2]) - deeplabcut.create_labeled_video( + successful = deeplabcut.create_labeled_video( path_config_file, [newvideo2], - destfolder=dfolder, + destfolder=DESTFOLDER, displaycropped=True, filtered=True, ) + assert all(successful), "Failed to create a labeled video!" print("Creating a Johansson video!") - deeplabcut.create_labeled_video( - path_config_file, [newvideo2], destfolder=dfolder, keypoints_only=True + successful = deeplabcut.create_labeled_video( + path_config_file, [newvideo2], destfolder=DESTFOLDER, keypoints_only=True ) + assert all(successful), "Failed to create a labeled video!" - deeplabcut.plot_trajectories( - path_config_file, [newvideo2], destfolder=dfolder, filtered=True - ) + deeplabcut.plot_trajectories(path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True) print("ALL DONE!!! - default cases without Tensorpack loader are functional.") @@ -349,8 +384,9 @@ def make_frame(t): deeplabcut.create_training_dataset( path_config_file, Shuffles=[2], - net_type=net_type, + net_type=NET, augmenter_type=augmenter_type3, + engine=engine, ) posefile = os.path.join( @@ -368,15 +404,15 @@ def make_frame(t): ) DLC_config = deeplabcut.auxiliaryfunctions.read_plainconfig(posefile) - DLC_config["save_iters"] = 10 + updated_max_iters = 10 + DLC_config["save_iters"] = updated_max_iters DLC_config["display_iters"] = 2 - DLC_config["multi_step"] = [[0.001, 10]] print("CHANGING training parameters to end quickly!") 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) + 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( @@ -388,9 +424,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( @@ -398,6 +432,7 @@ def make_frame(t): Shuffles=[4, 5], trainIndices=[trainIndices, trainIndices], testIndices=[testIndices, testIndices], + engine=engine, ) print("ALL DONE!!! - default cases are functional.") diff --git a/examples/testscript_transreid.py b/examples/testscript_transreid.py new file mode 100644 index 0000000000..2a0b270102 --- /dev/null +++ b/examples/testscript_transreid.py @@ -0,0 +1,348 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +import pickle +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", +] # "efficientnet-b0", "mobilenet_v2_0.35"] + +N_ITER = 5 +TESTTRACKER = "ellipse" + +USE_SHELVE = False # random.choice([True, False]) + +if __name__ == "__main__": + TASK = "multi_mouse" + SCORER = "dlc_team" + NUM_FRAMES = 5 + TRAIN_SIZE = 0.8 + + # NET = "dlcr101_ms5" + NET = "dlcrnet_ms5" + + # Always test a different model from list above + NET = random.choice(MODELS) + + basepath = os.path.dirname(os.path.realpath(__file__)) + DESTFOLDER = basepath + + video = "m3v1mp4" + 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) + + print("Project created.") + + print("Editing config...") + cfg = auxiliaryfunctions.edit_config( + config_path, + { + "numframes2pick": NUM_FRAMES, + "TrainingFraction": [TRAIN_SIZE], + "identity": True, + "uniquebodyparts": ["corner1", "corner2"], + }, + ) + print("Config edited.") + + print("Extracting frames...") + deeplabcut.extract_frames(config_path, mode="automatic", userfeedback=False) + print("Frames extracted.") + + print("Creating artificial data...") + rel_folder = os.path.join("labeled-data", os.path.splitext(video)[0]) + image_folder = os.path.join(cfg["project_path"], rel_folder) + n_animals = len(cfg["individuals"]) + ( + animals, + 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) + 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) + 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)) + 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") + print("Artificial data created.") + + print("Checking labels...") + deeplabcut.check_labels(config_path, draw_skeleton=False) + print("Labels checked.") + + print("Creating train dataset...") + 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 + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) + datafile, _ = auxiliaryfunctions.get_data_and_metadata_filenames( + trainingsetfolder, + 0.8, + 1, + cfg, + ) + datafile = datafile.with_suffix(".pickle") + with open(os.path.join(cfg["project_path"], datafile), "rb") as f: + pickledata = pickle.load(f) + num_images = len(pickledata) + 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"]) + pose_config_path = os.path.join(model_folder, "train", "pose_cfg.yaml") + edits = { + "global_scale": 0.5, + "batch_size": 1, + "save_iters": N_ITER, + "display_iters": N_ITER // 2, + "crop_size": [200, 200], + # "multi_step": [[0.001, N_ITER]], + } + deeplabcut.auxiliaryfunctions.edit_config(pose_config_path, edits) + print("Pose config edited.") + + print("Training network...") + deeplabcut.train_network(config_path, maxiters=N_ITER) + print("Network trained.") + + print("Evaluating network...") + deeplabcut.evaluate_network(config_path, plotting=True) + + print("Network evaluated....") + + print("Extracting maps...") + deeplabcut.extract_save_all_maps(config_path, Indices=[0, 1, 2]) + + new_video_path = deeplabcut.ShortenVideo( + video_path, + start="00:00:00", + stop="00:00:01", + outsuffix="short", + outpath=os.path.join(cfg["project_path"], "videos"), + ) + + print("Analyzing video...") + deeplabcut.analyze_videos( + config_path, + [new_video_path], + "mp4", + robust_nframes=True, + allow_growth=True, + use_shelve=USE_SHELVE, + ) + + print("Video analyzed.") + + print("Create video with all detections...") + scorer, _ = auxiliaryfunctions.get_scorer_name(cfg, 1, TRAIN_SIZE) + + deeplabcut.create_video_with_all_detections( + config_path, [new_video_path], shuffle=1, displayedbodyparts=["bodypart1"] + ) + + print("Video created.") + + print("Convert detections to tracklets...") + deeplabcut.convert_detections2tracklets(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) + print("Tracklets created...") + + ### adding it here + modelprefix = "" + ( + trainposeconfigfile, + testposeconfigfile, + snapshotfolder, + ) = deeplabcut.return_train_network_path(config_path, shuffle=1, modelprefix=modelprefix, trainingsetindex=0) + + print("Creating triplet dataset") + + deeplabcut.pose_estimation_tensorflow.create_tracking_dataset( + config_path, + [new_video_path], + TESTTRACKER, + video_extensions="mp4", + ) + + train_epochs = 10 + train_frac = 0.8 + + print("Training transformer") + + deeplabcut.pose_tracking_pytorch.train_tracking_transformer( + config_path, + scorer, + [new_video_path], + train_frac=train_frac, + modelprefix=modelprefix, + train_epochs=train_epochs, + ckpt_folder=snapshotfolder, + ) + + transformer_checkpoint = os.path.join(snapshotfolder, f"dlc_transreid_{train_epochs}.pth") + + print("Stitching tracklets based on transformer") + + deeplabcut.stitch_tracklets( + config_path, + [new_video_path], + "mp4", + track_method=TESTTRACKER, + transformer_checkpoint=transformer_checkpoint, + ) + + print("Plotting trajectories...") + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) + print("Trajectory plotted.") + + print("Creating labeled video...") + deeplabcut.create_labeled_video( + config_path, + [new_video_path], + "mp4", + save_frames=False, + color_by="individual", + track_method="transformer", + ) + print("Labeled video created.") + + print("Filtering predictions...") + 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) + print("Outlier frames extracted.") + + vname = Path(new_video_path).stem + + file = os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "machinelabels-iter" + str(cfg["iteration"]) + ".h5", + ) + + """ + 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 = DF.drop("likelihood", axis=1, level=3) + DF.to_csv( + os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "CollectedData_" + scorer + ".csv", + ) + ) + DF.to_hdf( + os.path.join( + cfg["project_path"], + "labeled-data", + vname, + "CollectedData_" + scorer + ".h5", + ), + key="df_with_missing", + format="table", + mode="w", + ) + """ + + print("MERGING") + deeplabcut.merge_datasets(config_path) # iteration + 1 + + print("CREATING TRAININGSET UPDATED TRAINING SET") + deeplabcut.create_training_dataset(config_path, Shuffles=[3], net_type=NET) + + print("TRAINING NETWORK...") + deeplabcut.train_network(config_path, shuffle=3, maxiters=N_ITER) + print("NETWORK TRAINED!") + + print("EVALUATING NETWORK...") + deeplabcut.evaluate_network(config_path, Shuffles=[3], plotting=True) + + print("NETWORK EVALUATED....") + + print("ANALYZING VIDEO WITH AUTO_TRACK....") + deeplabcut.analyze_videos( + config_path, + [new_video_path], + shuffle=3, + video_extensions="mp4", + save_as_csv=True, + destfolder=DESTFOLDER, + cropping=[0, 50, 0, 50], + allow_growth=True, + use_shelve=USE_SHELVE, + auto_track=True, + ) + + n_tracks = 3 + + print("TESTING THE UNIFIED API FOR TRANSFORMER") + + deeplabcut.transformer_reID( + config_path, + [new_video_path], + video_extensions="mp4", + shuffle=3, + n_tracks=n_tracks, + track_method=TESTTRACKER, + train_epochs=10, + n_triplets=10, + destfolder=DESTFOLDER, + ) + + print("CREATING LABELED VIDEOS (FOR ELLIPSE AND TRANSFORMER)...") + + deeplabcut.create_labeled_video( + config_path, + [new_video_path], + video_extensions="mp4", + shuffle=3, + track_method="ellipse", + destfolder=DESTFOLDER, + ) + + deeplabcut.create_labeled_video( + config_path, + [new_video_path], + video_extensions="mp4", + shuffle=3, + track_method="transformer", + destfolder=DESTFOLDER, + ) + + print("ALL DONE!!! - default multianimal cases are functional.") diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 0000000000..8ffb0fe637 --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,439 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +import shutil +import string +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import matplotlib + +matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows + +import cv2 +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 + + +def log_step(message: Any) -> None: + print(100 * "-") + print(str(message)) + print(100 * "-") + + +def cleanup(test_path: Path) -> None: + if test_path.exists(): + shutil.rmtree(test_path) + + +@dataclass(frozen=True) +class SyntheticProjectParameters: + multianimal: bool + num_bodyparts: int + num_frames: int = 10 + num_individuals: int = 1 + num_unique: int = 0 + identity: bool = False + frame_shape: tuple[int, int] = (480, 640) + + def bodyparts(self) -> list[str]: + return [i for i in string.ascii_lowercase[: self.num_bodyparts]] + + def unique(self) -> list[str]: + return [f"unique_{i}" for i in string.ascii_lowercase[: self.num_unique]] + + def individuals(self) -> list[str]: + return [f"animal_{i}" for i in range(self.num_individuals)] + + +def sample_pose_random( + gen: np.random.Generator, + num_individuals: int, + num_bodyparts: int, + num_unique: int, + img_h: int, + img_w: int, +) -> np.ndarray: + """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) + + image_data = pose.reshape(-1) + if num_unique > 0: + unique_pose = np.stack( + [ + gen.choice(img_w, size=(1, num_unique), replace=False), + gen.choice(img_h, size=(1, num_unique), replace=False), + ], + axis=-1, + ) + image_data = np.concatenate([image_data, unique_pose.reshape(-1)]) + return image_data + + +def sample_pose_from_center( + center_xs: np.ndarray, + center_ys: np.ndarray, + num_individuals: int, + num_bodyparts: int, + num_unique: int, + radius: int = 25, +) -> np.ndarray: + """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, 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 + pose[i, :, 0] = np.linspace(start=x_start, stop=x_end, num=num_bodyparts) + pose[i, :, 1] = np.linspace(start=y_start, stop=y_end, num=num_bodyparts) + + image_data = pose.reshape(-1) + if num_unique > 0: + xc, yc = center_xs[-1], center_ys[-1] + x_start, x_end = xc - radius + 1, xc + radius - 1 + y_start, y_end = yc - radius + 1, yc + radius - 1 + unique_pose = np.zeros((1, num_unique, 2)) + unique_pose[0, :, 0] = np.linspace(start=x_start, stop=x_end, num=num_unique) + unique_pose[0, :, 1] = np.linspace(start=y_start, stop=y_end, num=num_unique) + image_data = np.concatenate([image_data, unique_pose.reshape(-1)]) + return image_data + + +def gen_fake_data( + scorer: str, + video_name: str, + params: SyntheticProjectParameters, +) -> pd.DataFrame: + kpt_entries = ["x", "y"] + col_names = ["scorer", "individuals", "bodyparts", "coords"] + col_values = [] + for i in params.individuals(): + for b in params.bodyparts(): + col_values += [(scorer, i, b, entry) for entry in kpt_entries] + + for unique_bpt in params.unique(): + col_values += [(scorer, "single", unique_bpt, entry) for entry in kpt_entries] + + index_data = [] + pose_data = [] + gen = np.random.default_rng(seed=0) + + # sample starting points for each individual + img_h, img_w = params.frame_shape[:2] + radius = 8 + center_xs = gen.choice( + np.arange(radius, img_w - radius), + size=params.num_individuals + 1, # in case unique bodyparts + replace=False, + ) + center_ys = gen.choice( + np.arange(radius, img_h - radius), + size=params.num_individuals + 1, # in case unique bodyparts + replace=False, + ) + + for frame_index in range(params.num_frames): + index_data.append(("labeled-data", video_name, f"img{frame_index:04}.png")) + pose_data.append( + sample_pose_from_center( + center_xs, + center_ys, + num_individuals=params.num_individuals, + num_bodyparts=params.num_bodyparts, + num_unique=params.num_unique, + radius=radius, + ) + ) + mvt_x = gen.integers(low=-1, high=4, size=center_xs.size) + mvt_y = gen.integers(low=-1, high=4, size=center_ys.size) + center_xs = np.clip(center_xs + mvt_x, radius, img_w - radius) + center_ys = np.clip(center_ys + mvt_y, radius, img_h - radius) + + pose = np.stack(pose_data) + pose[params.num_frames // 2, :] = np.nan # add missing row in a frame + for idv in range(params.num_individuals): + idv_start = 2 * params.num_bodyparts * idv + idv_end = 2 * params.num_bodyparts * (idv + 1) + if params.num_frames > idv + 1: + pose[idv + 1, idv_start:idv_end] = np.nan + + for bpt in range(params.num_bodyparts): + frame_idx = 1 + params.num_individuals + bpt + idv_idx = bpt % params.num_individuals + offset = 2 * params.num_bodyparts * idv_idx + bpt_start, bpt_end = 2 * bpt + offset, 2 * (bpt + 1) + offset + if params.num_frames + 1 > frame_idx: + pose[frame_idx, bpt_start:bpt_end] = np.nan + + return pd.DataFrame( + pose, + index=pd.MultiIndex.from_tuples(index_data), + columns=pd.MultiIndex.from_tuples(col_values, names=col_names), + ) + + +def gen_fake_image( + project_root: Path, + row: pd.Series, + params: SyntheticProjectParameters, + radius: int = 5, +): + img_h, img_w = params.frame_shape + image_array = np.zeros((*params.frame_shape, 3), dtype=np.uint8) + for i, idv in enumerate(params.individuals()): + r = int(255 * (i + 1) / params.num_individuals) + if "individuals" in row.index.names: + idv_data = row.droplevel("scorer").loc[idv] + else: + idv_data = row.droplevel("scorer") + + keypoints = idv_data.to_numpy().reshape((-1, 2)) + if not np.all(np.isnan(keypoints)): + idv_center = np.nanmean(keypoints, axis=0) + x, y = int(idv_center[0]), int(idv_center[1]) + 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, 0] = r + + for j, bpt in enumerate(params.bodyparts()): + g = int(255 * (j + 1) / params.num_bodyparts) + + bpt_data = idv_data.loc[bpt] + if np.all(~pd.isnull(bpt_data)): + 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, 0] = r + image_array[ymin:ymax, xmin:xmax, 1] = g + + if params.num_unique > 0: + unique_data = row.droplevel("scorer").loc["single"] + for i, unique_bpt in enumerate(params.unique()): + bpt_data = unique_data.loc[unique_bpt] + if np.all(~pd.isnull(bpt_data)): + 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) + + img = Image.fromarray(image_array) + img.save(project_root / Path(*row.name)) + + +def generate_video_from_images(image_dir: Path, output_video: Path) -> None: + images = [p for p in image_dir.iterdir() if p.is_file() and p.suffix == ".png"] + images = sorted(images, key=lambda f: f.stem) + if len(images) == 0: + return + + height, width, channels = cv2.imread(str(images[0])).shape + fourcc = cv2.VideoWriter_fourcc(*"MJPG") + out = cv2.VideoWriter(str(output_video), fourcc, 10, (width, height)) + for img_path in images: + img = cv2.imread(str(img_path)) + out.write(img) + out.release() + + +def create_fake_project(path: Path, params: SyntheticProjectParameters) -> None: + if path.exists(): + raise ValueError("Cannot create a fake project at an existing path") + + scorer = "synthetic" + video_name = "cat" + path.mkdir(parents=True, exist_ok=False) + config = { + "Task": "synthetic", + "scorer": scorer, + "date": "Nov11", + "multianimalproject": params.multianimal, + "identity": params.identity, + "project_path": str(path / "config.yaml"), + "TrainingFraction": [0.8], + "iteration": 0, + "default_net_type": "resnet_50", + "default_augmenter": "default", + "default_track_method": "ellipse", + "snapshotindex": "all", + "batch_size": 8, + "pcutoff": 0.6, + "video_sets": { + str(path / "videos" / video_name): { + "crop": (0, params.frame_shape[1], 0, params.frame_shape[0]), + }, + }, + "start": 0, + "stop": 1, + "numframes2pick": 10, + "dotsize": 4, + "alphavalue": 1.0, + "colormap": "rainbow", + } + if not params.multianimal: + config["bodyparts"] = params.bodyparts() + assert params.num_individuals == 1 + assert params.num_unique == 0 + else: + config["bodyparts"] = "MULTI!" + config["multianimalbodyparts"] = params.bodyparts() + config["uniquebodyparts"] = params.unique() + config["individuals"] = params.individuals() + + af.write_config(str(path / "config.yaml"), config) + image_dir = path / "labeled-data" / video_name + image_dir.mkdir(parents=True, exist_ok=False) + + df = gen_fake_data( + scorer=scorer, + video_name=video_name, + params=params, + ) + print("SYNTHETIC DATA:") + print(df) + print("\n") + if not params.multianimal: + df.columns = df.columns.droplevel("individuals") + + df.to_hdf(image_dir / f"CollectedData_{scorer}.h5", key="df_with_missing") + df.to_csv(image_dir / f"CollectedData_{scorer}.csv") + + for idx in range(params.num_frames): + gen_fake_image(path, df.iloc[idx], params=params, radius=5) + + output_video = path / "videos" / "video.mp4" + output_video.parent.mkdir(exist_ok=True) + generate_video_from_images(image_dir, output_video) + + +def copy_project_for_test() -> Path: + data_path = Path.cwd() / "openfield-Pranav-2018-10-30" + test_path = Path.cwd() / "pytorch-testscript1234-openfield-Pranav-2018-10-30" + if not test_path.exists(): + shutil.copytree(data_path, test_path) + + project_config = af.read_config(str(test_path / "config.yaml")) + videos = list(project_config["video_sets"].keys()) + video = videos[0] + crop = project_config["video_sets"][video] + project_config["video_sets"] = {str(test_path / "videos" / "m3v1mp4.mp4"): crop} + af.write_config(str(test_path / "config.yaml"), project_config) + return test_path + + +def run( + config_path: Path, + train_fraction: float, + trainset_index: int, + net_type: str, + videos: list[str], + device: str, + engine: Engine = Engine.PYTORCH, + pytorch_cfg_updates: dict | None = None, + create_labeled_videos: bool = False, + ctd_conditions: tuple[int, int] | None = None, +) -> None: + times = [time.time()] + log_step(f"Testing with net type {net_type}") + log_step("Creating the training dataset") + deeplabcut.create_training_dataset(config_path, net_type=net_type, engine=engine, ctd_conditions=ctd_conditions) + 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}") + deeplabcut.train_network( + config=str(config_path), + shuffle=shuffle_index, + trainingsetindex=trainset_index, + device=device, + pytorch_cfg_updates=pytorch_cfg_updates, + ) + 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}") + deeplabcut.evaluate_network( + config=str(config_path), + Shuffles=[shuffle_index], + trainingsetindex=trainset_index, + device=device, + plotting=True, + per_keypoint_evaluation=True, + ) + times.append(time.time()) + log_step(f"Evaluation time: {times[-1] - times[-2]} seconds") + + 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) + 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") + + 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) + + # relaxed tracking parameters + deeplabcut.convert_detections2tracklets( + str(config_path), + **video_kwargs, + inferencecfg=dict( + boundingboxslack=10, + iou_threshold=0.2, + max_age=5, + method="m1", + min_hits=1, + minimalnumberofconnections=2, + pafthreshold=0.1, + pcutoff=0.1, + topktoretain=3, + variant=0, + withid=False, + ), + ) + deeplabcut.stitch_tracklets(str(config_path), **video_kwargs, min_length=3) + + if create_labeled_videos: + log_step(f"Making labeled video, {train_fraction}, shuffle={shuffle_index}") + results = deeplabcut.create_labeled_video( + config=str(config_path), + videos=videos, + shuffle=shuffle_index, + trainingsetindex=trainset_index, + ) + assert all(results), f"Failed to create some labeled video for {videos}" + + +if __name__ == "__main__": + create_fake_project( + path=Path("synthetic-data-niels"), + params=SyntheticProjectParameters( + multianimal=True, + num_bodyparts=4, + num_individuals=3, + num_unique=1, + num_frames=50, + frame_shape=(128, 256), + ), + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..cec3b6077a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,247 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ "setuptools>=61" ] + +[project] +name = "deeplabcut" +version = "3.0.1" +description = "Markerless pose-estimation of user-defined features with deep learning" +readme = { file = "README.md", content-type = "text/markdown" } +keywords = [ + "animal behavior", + "markerless tracking", + "neuroscience", + "pose estimation", +] +license = { text = "LGPL-3.0-or-later" } +requires-python = ">=3.10" +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Image Processing", +] +dependencies = [ + "albumentations<=1.4.3", + "dlclibrary>=0.0.12", + "einops", + "filelock>=3.12,<3.16", + "filterpy>=1.4.4", + "h5py>=3.15.1; platform_system=='Darwin'", + "huggingface-hub>=0.23", + "imageio-ffmpeg", + "imgaug>=0.4", + "matplotlib>=3.3,<3.9,!=3.7,!=3.7.1", + "networkx>=2.6", + "numba>=0.54", + "numpy>=1.18.5,<2", + "packaging>=26", + # Migration to pandas 3.0 is tracked in https://github.com/DeepLabCut/DeepLabCut/issues/3362. + "pandas[hdf5,performance]>=2.2,<3", + "pillow>=7.1", + "pycocotools", + "pydantic>=2,<3", + "pyyaml", + "ruamel-yaml>=0.15", + "scikit-image>=0.17", + "scikit-learn>=1", + "scipy>=1.9", + "statsmodels>=0.11", + "tables>3.8", + "timm", + "torch>=2", + "torchvision", + "tqdm", +] +[[project.authors]] +name = "M-Lab of Adaptive Intelligence" +email = "mackenzie@deeplabcut.org" +[[project.authors]] +name = "Mathis Group for Computational Neuroscience and AI" +email = "alexander@deeplabcut.org" +[project.optional-dependencies] +gui = [ + "napari-deeplabcut>=0.3.1", + "pyside6; platform_system!='Linux' or platform_machine!='x86_64'", + # Avoid 6.10.0 only on Linux x86_64 (fails for older glib versions) + "pyside6<6.10; platform_system=='Linux' and platform_machine=='x86_64'", + "qdarkstyle==3.1; platform_system=='Linux' and platform_machine=='x86_64'", + "qdarkstyle>=3.1; platform_system!='Linux' or platform_machine!='x86_64'", +] +openvino = [ "openvino-dev==2022.1" ] +docs = [ + "jupyter-book==1.0.4.post1", + "sphinxcontrib-mermaid", +] +dev-docs = [ + "black>=24", + "mike>=2.1", + "mkdocs>=1.6", + "mkdocs-api-autonav>=0.1", + "mkdocs-autorefs>=1.2", + "mkdocs-jupyter>=0.25", + "mkdocs-material[imaging]>=9.5", + "mkdocstrings[python]>=0.27", +] +fmpose3d = [ "fmpose3d>=0.0.8" ] +# Use only one of [tf, tf-cu11, tf-cu12, tf-latest]. Do not combine extras. +tf = [ + "protobuf<7", + "tensorflow>=2.12,<2.16; python_version<'3.12'", + "tensorflow>=2.16.1,<2.18; python_version>='3.12'", + "tensorflow-io-gcs-filesystem==0.31; platform_system=='Windows' and python_version<'3.12'", + "tensorflow-metal==1.2; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow-metal>=1.2; platform_system=='Darwin' and python_version>='3.12'", + "tensorpack>=0.11", + "tf-keras<2.15; python_version<'3.12'", + "tf-keras>=2.15,<2.18; python_version>='3.12'", + "tf-slim>=1.1", +] +tf-cu11 = [ + "protobuf<7", + "tensorflow==2.14", + "tensorflow-io-gcs-filesystem==0.31; platform_system=='Windows'", + "tensorflow-metal==1.2; platform_system=='Darwin'", + "tensorpack==0.11", + "tf-keras==2.14.1", + "tf-slim==1.1", + "torch<2.1", + "torchvision<0.16", +] +tf-cu12 = [ + "protobuf<7", + "tensorflow==2.18", + "tensorflow-metal==1.2; platform_system=='Darwin'", + "tensorpack==0.11", + "tf-keras==2.18", + "tf-slim==1.1", + "torch<2.11", + "torchvision<0.26", +] +tf-latest = [ + "protobuf<7", + "tensorflow>=2.18", + "tensorflow-metal>=1.2; platform_system=='Darwin'", + "tensorpack>=0.11", + "tf-keras", + "tf-slim>=1.1", +] +# apple_mchips is kept for older systems, prefer [tf] in new projects. +apple_mchips = [ + "protobuf<7; platform_system=='Darwin'", + "tensorflow>=2.12,<2.15; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow>=2.15,<2.18; platform_system=='Darwin' and python_version>='3.12'", + "tensorflow-metal==1.2; platform_system=='Darwin' and python_version<'3.12'", + "tensorflow-metal>=1.2; platform_system=='Darwin' and python_version>='3.12'", + "tensorpack>=0.11; platform_system=='Darwin'", + "tf-keras; platform_system=='Darwin'", + "tf-slim>=1.1; platform_system=='Darwin'", +] +modelzoo = [ "huggingface-hub" ] +wandb = [ "wandb" ] +[project.scripts] +dlc = "deeplabcut.__main__:main" +[project.urls] +Homepage = "https://www.deeplabcut.org" +Repository = "https://github.com/DeepLabCut/DeepLabCut" +Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" + +[dependency-groups] +dev = [ + "coverage", + "nbformat>5", + "pre-commit", + "pytest", + "pytest-cov", + "ruff", +] +gui-dev = [ + "pytest-qt", +] + +[tool.setuptools] +include-package-data = false +[tool.setuptools.package-data] +"*" = [ "*.yaml", "*.yml", "*.json", "*.qss", "*.png", "*.md", "*.sh" ] +[tool.setuptools.packages.find] +include = [ "deeplabcut*" ] +exclude = [ "tests*", "docs*", "examples*" ] + +[tool.uv] +# One of tf / tf-cu12 / tf-latest. apple_mchips matches [tf] on macOS but conflicts with +# [tf-cu12] and [tf-latest] (overlapping tensorflow pins cannot be unified with uv's lock). +conflicts = [ + [ + { extra = "tf" }, + { extra = "tf-cu11" }, + { extra = "tf-cu12" }, + { extra = "tf-latest" }, + { extra = "apple_mchips" }, + ], + [ + { extra = "tf-cu11" }, + { extra = "tf-cu12" }, + { extra = "fmpose3d" }, + ], +] +[[tool.uv.dependency-metadata]] +name = "openvino-dev" +version = "2022.1.0" +requires-dist = [] +[tool.uv.pip] +torch-backend = "auto" + +[tool.ruff] +target-version = "py310" +line-length = 120 +fix = true +[tool.ruff.format] +docstring-code-format = true +[tool.ruff.lint] +select = [ "E", "F", "B", "I", "UP", "PIE" ] +ignore = [ "E741", "B007" ] +[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" + +# Config for the docformatter pre-commit hook (currently disabled in favour of ruff). +# Re-enable the hook in .pre-commit-config.yaml when large-scale docstring +# reformatting is needed. +[tool.docformatter] +wrap-descriptions = 88 +wrap-summaries = 88 +black = true + +[tool.pyproject-fmt] +max_supported_python = "3.12" +generate_python_version_classifiers = true +# Avoid collapsing tables to field.key = value format (less readable) +table_format = "long" + +[tool.pytest.ini_options] +markers = [ + "require_models: mark test as requiring models to run", + "fmpose3d: tests for fmpose3d integration", + "unittest: fast unit-level tests", + "functional: functional/integration-style tests", + "deprecated: tests for deprecated APIs kept for backward-compatibility", +] + +[tool.mdformat] +# Preserve prose line breaks; avoid spurious diffs from reflowing paragraphs. +wrap = "no" +end_of_line = "lf" diff --git a/reinstall.sh b/reinstall.sh index e2a033256c..ec60dc6a8e 100755 --- a/reinstall.sh +++ b/reinstall.sh @@ -1,3 +1,4 @@ pip uninstall deeplabcut +rm -rf dist/ build/ *.egg-info python3 setup.py sdist bdist_wheel -pip install dist/deeplabcut-2.2rc1-py3-none-any.whl +pip install dist/deeplabcut-3.0.0-py3-none-any.whl diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 93baa5c643..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,32 +0,0 @@ -python-dateutil -ipython -ipython-genutils -wheel -certifi -chardet -click -cython -filterpy -h5py -ruamel.yaml>=0.15.0 -intel-openmp -imgaug -numba==0.51.1 -matplotlib==3.1.3 -networkx -numpy~=1.17.3 -opencv-python-headless~=3.4.9.33 -pandas>=1.0.1 -patsy -pyyaml -setuptools -scikit-image>=0.17 -scikit-learn -scipy>=1.4 -six -statsmodels>=0.11 -tables -tensorpack==0.9.8 -tqdm -moviepy<=1.0.1 -Pillow>=7.1 diff --git a/setup.py b/setup.py index dd945fe2fc..c3e301e85e 100644 --- a/setup.py +++ b/setup.py @@ -1,90 +1,12 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut +"""DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) © A. -Please see AUTHORS for contributors. -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +& 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 """ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="deeplabcut", - version="2.2rc1", - author="A. & M. Mathis Labs", - author_email="alexander@deeplabcut.org", - description="Markerless pose-estimation of user-defined features with deep learning", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/DeepLabCut/DeepLabCut", - install_requires=[ - "python-dateutil", - "ipython", - "ipython-genutils", - "wheel", - "certifi", - "chardet", - "click", - "cython", - "filterpy", - "h5py", - "ruamel.yaml>=0.15.0", - "intel-openmp", - "imgaug", - "numba==0.51.1", - "matplotlib==3.1.3", - "networkx", - "numpy~=1.17.3", - "opencv-python-headless~=3.4.9.33", - "pandas>=1.0.1", - "patsy", - "pyyaml", - "setuptools", - "scikit-image>=0.17", - "scikit-learn", - "scipy>=1.4", - "six", - "statsmodels>=0.11", - "tables", - "tensorpack==0.9.8", - "tqdm", - "moviepy<=1.0.1", - "Pillow>=7.1", - ], - extras_require={"gui": ["wxpython<4.1"]}, - scripts=["deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh"], - packages=setuptools.find_packages(), - data_files=[ - ( - "deeplabcut", - [ - "deeplabcut/pose_cfg.yaml", - "deeplabcut/inference_cfg.yaml", - "deeplabcut/pose_estimation_tensorflow/models/pretrained/pretrained_model_urls.yaml", - "deeplabcut/gui/media/logo.png", - "deeplabcut/gui/media/dlc_1-01.png", - "deeplabcut/pose_estimation_tensorflow/lib/nms_cython/nms_grid.pyx", - "deeplabcut/pose_estimation_tensorflow/lib/nms_cython/nms_grid.cpp", - "deeplabcut/pose_estimation_tensorflow/lib/nms_cython/include/nms_scoremap.hxx", - "deeplabcut/pose_estimation_tensorflow/lib/nms_cython/include/andres/marray.hxx", - ], - ) - ], - include_package_data=True, - classifiers=( - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - "Operating System :: OS Independent", - ), - entry_points="""[console_scripts] - dlc=dlc:main""", -) +from setuptools import setup -# https://www.python.org/dev/peps/pep-0440/#compatible-release +# All configuration is now in pyproject.toml. This file is kept for backward compatibility +setup() diff --git a/tests/conftest.py b/tests/conftest.py index 4495581001..9c4b5c3597 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,36 +1,159 @@ -import numpy as np +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 import pickle +import urllib.request +import zipfile +from io import BytesIO + +import numpy as np import pytest -from deeplabcut.pose_estimation_tensorflow.lib import inferenceutils, crossvalutils +from PIL import Image +from tqdm import tqdm + +# Enable pandas future mode warnings if DLC_PANDAS_FUTURE env var is set +from deeplabcut.utils.pandas_future_mode import configure_pandas_future_if_enabled + +configure_pandas_future_if_enabled() + +from deeplabcut.core import inferenceutils # noqa: E402 + +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: 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) + with zipfile.ZipFile(BytesIO(resp.read())) as zf: + for member in tqdm(zf.infolist(), desc="Extracting"): + try: + zf.extract(member, path=dest_folder) + except zipfile.error: + pass -TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") +def _test_data_ready() -> bool: + 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="session") +def test_data_dir(): + """Path to shared test data under tests/data/.""" + return TEST_DATA_DIR + + +@pytest.fixture(scope="function") +def ground_truth_detections(): + with open(os.path.join(TEST_DATA_DIR, "dets.pickle"), "rb") as file: + return pickle.load(file) + + +@pytest.fixture(scope="function") +def model_outputs(): + with open(os.path.join(TEST_DATA_DIR, "outputs.pickle"), "rb") as file: + scmaps, locrefs, pafs = pickle.load(file) + locrefs = np.reshape(locrefs, (*locrefs.shape[:3], -1, 2)) + locrefs *= 7.2801 + pafs = np.reshape(pafs, (*pafs.shape[:3], -1, 2)) + return scmaps, locrefs, pafs + + +@pytest.fixture(scope="function") +def sample_image(): + return np.asarray(Image.open(os.path.join(TEST_DATA_DIR, "image.png"))) + + +@pytest.fixture(scope="function") +def sample_keypoints(): + with open(os.path.join(TEST_DATA_DIR, "trimouse_assemblies.pickle"), "rb") as file: + temp = pickle.load(file) + return np.concatenate(temp[0])[:, :2] + + +@pytest.fixture(scope="function") def real_assemblies(): with open(os.path.join(TEST_DATA_DIR, "trimouse_assemblies.pickle"), "rb") as file: temp = pickle.load(file) data = np.stack(list(temp.values())) - return inferenceutils._parse_ground_truth_data(data[..., :3]) + return inferenceutils._parse_ground_truth_data(data) -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") +def real_assemblies_montblanc(): + with open(os.path.join(TEST_DATA_DIR, "montblanc_assemblies.pickle"), "rb") as file: + temp = pickle.load(file) + single = temp.pop("single") + data = np.full((max(temp) + 1, 3, 4, 4), np.nan) + for k, assemblies in temp.items(): + for i, assembly in enumerate(assemblies): + data[k, i] = assembly + return inferenceutils._parse_ground_truth_data(data), single + + +@pytest.fixture(scope="function") def real_tracklets(): with open(os.path.join(TEST_DATA_DIR, "trimouse_tracklets.pickle"), "rb") as file: return pickle.load(file) -@pytest.fixture(scope="session") -def uncropped_data_and_metadata(): +@pytest.fixture(scope="function") +def real_tracklets_montblanc(): + with open(os.path.join(TEST_DATA_DIR, "montblanc_tracklets.pickle"), "rb") as file: + return pickle.load(file) + + +@pytest.fixture(scope="function") +def evaluation_data_and_metadata(): full_data_file = os.path.join(TEST_DATA_DIR, "trimouse_eval.pickle") metadata_file = full_data_file.replace("eval", "meta") with open(full_data_file, "rb") as file: data = pickle.load(file) with open(metadata_file, "rb") as file: metadata = pickle.load(file) - params = crossvalutils._set_up_evaluation(data) - data_unc, _ = crossvalutils._rebuild_uncropped_data(data, params) - meta_unc = crossvalutils._rebuild_uncropped_metadata(metadata, params["imnames"]) - return data_unc, meta_unc + return data, metadata + + +@pytest.fixture(scope="function") +def evaluation_data_and_metadata_montblanc(): + full_data_file = os.path.join(TEST_DATA_DIR, "montblanc_eval.pickle") + metadata_file = full_data_file.replace("eval", "meta") + with open(full_data_file, "rb") as file: + data = pickle.load(file) + with open(metadata_file, "rb") as file: + metadata = pickle.load(file) + return data, metadata diff --git a/tests/core/config/conftest.py b/tests/core/config/conftest.py new file mode 100644 index 0000000000..6f62520f50 --- /dev/null +++ b/tests/core/config/conftest.py @@ -0,0 +1,45 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Shared fixtures for config tests (no import-time migration registration).""" + +import pytest + +from deeplabcut.core.config import versioning +from deeplabcut.core.config.versioning import register_migration + +_TOY_VERSION_OLD = 98 +_TOY_VERSION_NEW = 99 +_LEGACY_FIELD = "toy_legacy_field" +_NEW_FIELD = "toy_new_field" + + +@pytest.fixture(autouse=True) +def isolated_migration_registry(monkeypatch): + """Snapshot the migration registry per test; production migrations only at baseline.""" + monkeypatch.setattr(versioning, "_MIGRATIONS", versioning._MIGRATIONS.copy()) + + +@pytest.fixture +def register_toy_migrations(monkeypatch): + """Register v98 <-> v99 toy migrations for the given config_type(s).""" + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", _TOY_VERSION_NEW) + + def register_pair(config_type: str) -> None: + @register_migration(_TOY_VERSION_OLD, _TOY_VERSION_NEW, config_type=config_type) + def up(config: dict) -> dict: + if _LEGACY_FIELD in config: + config[_NEW_FIELD] = config.pop(_LEGACY_FIELD) + return config + + @register_migration(_TOY_VERSION_NEW, _TOY_VERSION_OLD, config_type=config_type) + def down(config: dict) -> dict: + if _NEW_FIELD in config: + config[_LEGACY_FIELD] = config.pop(_NEW_FIELD) + return config + + return register_pair diff --git a/tests/core/config/test_base_config.py b/tests/core/config/test_base_config.py new file mode 100644 index 0000000000..4173bfd8fd --- /dev/null +++ b/tests/core/config/test_base_config.py @@ -0,0 +1,493 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for DLCBaseConfig.""" + +import pytest +from pydantic import Field, ValidationError + +from deeplabcut.core.config import DLCBaseConfig, DLCVersionedConfig +from deeplabcut.core.deprecation import DLCDeprecationWarning + +_TOY_VERSION_OLD = 98 +_TOY_VERSION_NEW = 99 +_LEGACY_FIELD = "toy_legacy_field" + + +class ToyConfig(DLCBaseConfig): + """Minimal config used to exercise DLCBaseConfig.""" + + Task: str = "DefaultTask" + project_path: str = Field( + default="DefaultProjectPath", + json_schema_extra={"aliases": ["projectPath"]}, + ) + + +class NestedInner(DLCBaseConfig): + lr: float = 0.001 + momentum: float = 0.9 + + +class NestedOuter(DLCBaseConfig): + name: str = "outer" + inner: NestedInner | None = Field(default_factory=NestedInner) + + +class DictBranchConfig(DLCBaseConfig): + payload: dict = Field(default_factory=lambda: {"section": {"key": "old", "count": 1}}) + + +# ------------------------------------------------------------------ +# Dict-like access protocol +# ------------------------------------------------------------------ + + +class TestGetitem: + def test_getitem_returns_field_value(self): + cfg = ToyConfig(Task="t", project_path="/p") + assert cfg["Task"] == "t" + assert cfg["project_path"] == "/p" + + def test_getitem_missing_key_raises_key_error(self): + cfg = ToyConfig() + with pytest.raises(KeyError): + cfg["nonexistent"] + + +class TestSetitem: + def test_setitem_updates_field(self): + cfg = ToyConfig() + cfg["Task"] = "new_task" + assert cfg.Task == "new_task" + assert cfg["Task"] == "new_task" + + def test_setitem_unknown_field_raises_key_error(self): + cfg = ToyConfig() + with pytest.raises(KeyError, match="no field"): + cfg["nonexistent"] = 42 + + +class TestContains: + def test_contains_existing_field(self): + cfg = ToyConfig() + assert "Task" in cfg + assert "project_path" in cfg + + def test_contains_missing_field(self): + cfg = ToyConfig() + assert "nonexistent" not in cfg + + def test_contains_non_string_returns_false(self): + cfg = ToyConfig() + assert 42 not in cfg + + +class TestGet: + def test_get_existing_key(self): + cfg = ToyConfig(Task="hello") + assert cfg.get("Task") == "hello" + + def test_get_missing_key_returns_default(self): + cfg = ToyConfig() + assert cfg.get("nonexistent") is None + assert cfg.get("nonexistent", 42) == 42 + + +class TestUpdate: + def test_update_applies_fields_dict(self): + cfg = ToyConfig() + cfg.update({"Task": "t", "project_path": "/p"}) + assert cfg.Task == "t" + assert cfg.project_path == "/p" + + def test_update_applies_fields_kwargs(self): + cfg = ToyConfig() + cfg.update(Task="t", project_path="/p") + assert cfg.Task == "t" + assert cfg.project_path == "/p" + + def test_update_empty_is_noop(self): + cfg = ToyConfig(Task="unchanged") + assert cfg.update() is cfg + assert cfg.update({}) is cfg + assert cfg.Task == "unchanged" + + def test_update_rejects_dict_and_kwargs_together(self): + cfg = ToyConfig() + with pytest.raises(TypeError, match="either a dict or keyword"): + cfg.update({"Task": "x"}, Task="y") + + def test_update_unknown_field_raises_validation_error(self): + cfg = ToyConfig() + with pytest.raises(ValidationError): + cfg.update({"nonexistent": 42}) + + def test_update_invalid_value_raises(self): + cfg = TypedBase() + with pytest.raises(ValidationError): + cfg.update({"count": "not-a-number"}) + assert cfg.count == 0 + + def test_update_resolves_alias_with_warning(self): + cfg = ToyConfig() + with pytest.warns(DLCDeprecationWarning, match="projectPath") as record: + cfg.update({"projectPath": "/alias"}) + assert len(record) == 1 + assert cfg.project_path == "/alias" + + def test_update_rejects_alias_and_canonical_together(self): + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + with pytest.raises(TypeError, match=r"projectPath.*project_path"): + ToyConfig().update({"projectPath": "/a", "project_path": "/b"}) + + +class TestKeysValuesItems: + def test_keys(self): + cfg = ToyConfig(Task="t", project_path="/p") + assert cfg.keys() == ["Task", "project_path"] + + def test_values(self): + cfg = ToyConfig(Task="t", project_path="/p") + assert cfg.values() == ["t", "/p"] + + def test_items(self): + cfg = ToyConfig(Task="t", project_path="/p") + assert cfg.items() == [("Task", "t"), ("project_path", "/p")] + + +class TestIterAndLen: + def test_iter_yields_keys(self): + cfg = ToyConfig() + assert list(cfg) == ["Task", "project_path"] + + def test_len(self): + cfg = ToyConfig() + assert len(cfg) == 2 + + def test_dict_constructor(self): + cfg = ToyConfig(Task="t", project_path="/p") + d = dict(cfg.items()) + assert d == {"Task": "t", "project_path": "/p"} + + +# ------------------------------------------------------------------ +# select() utility +# ------------------------------------------------------------------ + + +class TestSelect: + def test_select_single_level(self): + cfg = ToyConfig(Task="t") + assert cfg.select("Task") == "t" + + def test_select_nested_config(self): + cfg = NestedOuter(name="outer", inner=NestedInner(lr=0.01)) + assert cfg.select("inner.lr") == 0.01 + assert cfg.select("inner.momentum") == 0.9 + + def test_select_missing_returns_default(self): + cfg = ToyConfig() + assert cfg.select("nonexistent") is None + assert cfg.select("nonexistent", "fallback") == "fallback" + + def test_select_deep_missing_returns_default(self): + cfg = NestedOuter() + assert cfg.select("inner.nonexistent") is None + assert cfg.select("nope.deep.path", 0) == 0 + + def test_select_none_intermediate(self): + cfg = NestedOuter(inner=None) + assert cfg.select("inner.lr") is None + + +# ------------------------------------------------------------------ +# set_nested() utility +# ------------------------------------------------------------------ + + +class TestSetNested: + def test_set_nested_top_level_field(self): + cfg = ToyConfig() + cfg.set_nested("Task", "updated") + assert cfg.Task == "updated" + assert cfg.select("Task") == "updated" + + def test_set_nested_nested_typed_field(self): + cfg = NestedOuter(inner=NestedInner(lr=0.001)) + cfg.set_nested("inner.lr", 0.05) + assert cfg.inner.lr == 0.05 + assert cfg.select("inner.lr") == 0.05 + + def test_set_nested_dict_field(self): + cfg = DictBranchConfig() + cfg.set_nested("payload.section.key", "new") + cfg.set_nested("payload.section.count", 42) + assert cfg.select("payload.section.key") == "new" + assert cfg.select("payload.section.count") == 42 + + def test_set_nested_returns_self_for_nested_path(self): + cfg = NestedOuter() + assert cfg.set_nested("inner.lr", 0.02) is cfg + + def test_set_nested_missing_path_raises(self): + cfg = NestedOuter() + with pytest.raises(AttributeError, match="has no 'inner.missing'"): + cfg.set_nested("inner.missing", 1.0) + + def test_set_nested_none_intermediate_raises(self): + cfg = NestedOuter(inner=None) + with pytest.raises(AttributeError, match="has no 'inner.lr'"): + cfg.set_nested("inner.lr", 0.1) + + def test_set_nested_invalid_value_raises(self): + cfg = NestedOuter() + with pytest.raises(ValidationError): + cfg.set_nested("inner.lr", "not-a-float") + assert cfg.inner.lr == 0.001 + + def test_set_nested_unknown_top_level_field_raises(self): + cfg = ToyConfig() + with pytest.raises(ValidationError): + cfg.set_nested("nonexistent", 42) + + +# ------------------------------------------------------------------ +# model_validate / extra=forbid +# ------------------------------------------------------------------ + + +class TestModelValidate: + def test_model_validate_returns_instance(self): + cfg = ToyConfig.model_validate({"Task": "task", "project_path": ""}) + assert isinstance(cfg, ToyConfig) + assert cfg.Task == "task" + + def test_model_validate_rejects_extra_keys(self): + with pytest.raises(ValidationError): + ToyConfig.model_validate({"Task": "ok", "project_path": "", "extra": 1}) + + +# ------------------------------------------------------------------ +# aliases +# ------------------------------------------------------------------ + + +class TestAliases: + def test_model_validate_resolves_alias_without_from_dict(self): + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + cfg = ToyConfig.model_validate({"Task": "t", "projectPath": "/alias"}) + assert cfg.project_path == "/alias" + + def test_model_validate_rejects_alias_and_canonical_together(self): + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + with pytest.raises(TypeError, match=r"projectPath.*project_path"): + ToyConfig.model_validate({"Task": "t", "projectPath": "/alias", "project_path": "/canonical"}) + + def test_getitem_alias_warns_and_reads_canonical(self): + cfg = ToyConfig(project_path="/p") + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + assert cfg["projectPath"] == "/p" + + def test_setitem_alias_warns_once_and_writes_canonical(self): + cfg = ToyConfig() + with pytest.warns(DLCDeprecationWarning, match="projectPath") as record: + cfg["projectPath"] = "/new" + assert len(record) == 1 + assert cfg.project_path == "/new" + + def test_setattr_alias_warns_and_validates(self): + cfg = ToyConfig() + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + cfg.projectPath = "/attr" + assert cfg.project_path == "/attr" + + def test_getattr_alias_warns(self): + cfg = ToyConfig(project_path="/p") + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + assert cfg.projectPath == "/p" + + def test_contains_accepts_alias(self): + cfg = ToyConfig() + assert "projectPath" in cfg + + def test_keys_does_not_include_alias(self): + """keys() / iter() only yield canonical field names, never alias names.""" + cfg = ToyConfig() + assert "projectPath" not in cfg.keys() + assert "projectPath" not in list(cfg) + + def test_to_dict_uses_canonical_names_only(self): + """to_dict() always emits canonical field names, even when loaded via alias.""" + with pytest.warns(DLCDeprecationWarning): + cfg = ToyConfig.model_validate({"Task": "t", "projectPath": "/alias"}) + d = cfg.to_dict() + assert "project_path" in d + assert "projectPath" not in d + + def test_from_yaml_with_alias_key_warns_and_loads(self, tmp_path): + """A YAML file that contains an alias key emits a warning on load.""" + path = tmp_path / "config.yaml" + path.write_text("Task: t\nprojectPath: /from_yaml\n") + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + cfg = ToyConfig.from_yaml(path) + assert cfg.project_path == "/from_yaml" + + def test_to_yaml_then_from_yaml_no_alias_warning(self, tmp_path): + """After saving via to_yaml, reloading must not emit any alias warning + (the YAML file should contain canonical field names).""" + import warnings + + path = tmp_path / "config.yaml" + cfg = ToyConfig(Task="t", project_path="/p") + cfg.to_yaml(path) + + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + loaded = ToyConfig.from_yaml(path) + assert loaded.project_path == "/p" + + +# ------------------------------------------------------------------ +# validate_assignment on DLCBaseConfig (not just versioned) +# ------------------------------------------------------------------ + + +class TypedBase(DLCBaseConfig): + count: int = 0 + + +class TestBaseConfigValidateAssignment: + def test_invalid_assignment_raises(self): + cfg = TypedBase() + with pytest.raises(ValidationError): + cfg.count = "not-a-number" + assert cfg.count == 0 + + def test_valid_assignment_takes_effect(self): + cfg = TypedBase() + cfg.count = 7 + assert cfg.count == 7 + + def test_coercion_applied(self): + cfg = TypedBase() + cfg.count = True # coerced to 1 + assert cfg.count == 1 + + +# ------------------------------------------------------------------ +# from_dict / from_any / from_yaml / to_dict / to_yaml +# ------------------------------------------------------------------ + + +def test_from_dict_returns_instance(): + cfg = ToyConfig.from_dict({"Task": "t", "project_path": "/p"}) + assert isinstance(cfg, ToyConfig) + assert cfg.Task == "t" + assert cfg.project_path == "/p" + + +def test_from_dict_incomplete_uses_defaults(): + cfg = ToyConfig.from_dict({"Task": "custom_task"}) + assert cfg.Task == "custom_task" + assert cfg.project_path == "DefaultProjectPath" + + cfg_empty = ToyConfig.from_dict({}) + assert cfg_empty.Task == "DefaultTask" + assert cfg_empty.project_path == "DefaultProjectPath" + + +def test_from_any_with_instance_returns_same(): + cfg = ToyConfig.from_dict({"Task": "x", "project_path": ""}) + out = ToyConfig.from_any(cfg) + assert out is cfg + + +def test_from_any_with_dict_returns_instance(): + cfg = ToyConfig.from_any({"Task": "y", "project_path": ""}) + assert isinstance(cfg, ToyConfig) + assert cfg.Task == "y" + + +def test_from_any_with_invalid_type_raises(): + with pytest.raises(TypeError, match="Expected.*Got "): + ToyConfig.from_any(42) + + +def test_from_yaml_loads_file(tmp_path): + path = tmp_path / "config.yaml" + path.write_text("Task: mytask\nproject_path: /foo\n") + cfg = ToyConfig.from_yaml(path) + assert cfg.Task == "mytask" + assert cfg.project_path == "/foo" + + +def test_to_dict_returns_dict(): + cfg = ToyConfig.from_dict({"Task": "t", "project_path": ""}) + d = cfg.to_dict() + assert isinstance(d, dict) + assert d["Task"] == "t" + + +def test_from_dict_to_dict_roundtrip(): + d = {"Task": "rt", "project_path": "/x"} + cfg = ToyConfig.from_dict(d) + out = cfg.to_dict() + restored = ToyConfig.from_dict(out) + assert restored.Task == cfg.Task + assert restored.project_path == cfg.project_path + + +def test_to_yaml_roundtrip(tmp_path): + path = tmp_path / "out.yaml" + orig = ToyConfig.from_dict({"Task": "rt", "project_path": "/x"}) + orig.to_yaml(path) + loaded = ToyConfig.from_yaml(path) + assert loaded.Task == orig.Task + assert loaded.project_path == orig.project_path + + +def test_print_no_error(capsys): + cfg = ToyConfig.from_dict({"Task": "p", "project_path": ""}) + cfg.print() + out, _ = capsys.readouterr() + assert "Task" in out + + +# ------------------------------------------------------------------ +# No schema migration on DLCBaseConfig +# ------------------------------------------------------------------ + + +class _ToyBaseOnly(DLCBaseConfig): + toy_new_field: str = "" + + +class _ToyVersioned(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + toy_new_field: str = "" + + +class TestNoMigrationOnBaseConfig: + """DLCBaseConfig resolves aliases but does not run version migrations.""" + + def test_base_config_rejects_legacy_key_without_migration(self): + legacy_cfg = { + "config_version": _TOY_VERSION_OLD, + _LEGACY_FIELD: "value", + } + with pytest.raises(ValidationError): + _ToyBaseOnly.from_dict(legacy_cfg) + + def test_versioned_config_applies_migration_for_same_input(self, register_toy_migrations): + register_toy_migrations("_ToyVersioned") + legacy_cfg = { + "config_version": _TOY_VERSION_OLD, + _LEGACY_FIELD: "value", + } + cfg = _ToyVersioned.from_dict(legacy_cfg) + assert cfg.toy_new_field == "value" diff --git a/tests/core/config/test_change_tracking.py b/tests/core/config/test_change_tracking.py new file mode 100644 index 0000000000..d2f2a90dcc --- /dev/null +++ b/tests/core/config/test_change_tracking.py @@ -0,0 +1,333 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for change tracking on DLCVersionedConfig.""" + +from __future__ import annotations + +import logging + +import pytest +from pydantic import Field, ValidationError + +from deeplabcut.core.config import DLCVersionedConfig +from deeplabcut.core.deprecation import DLCDeprecationWarning + + +class TrackedConfig(DLCVersionedConfig): + name: str = "default" + count: int = 0 + flag: bool = False + + +# ------------------------------------------------------------------ +# Dirty-state tracking +# ------------------------------------------------------------------ + + +class TestDirtyState: + def test_clean_after_construction(self): + cfg = TrackedConfig() + assert not cfg.is_dirty + assert cfg.dirty_fields == frozenset() + + def test_dirty_after_field_change(self): + cfg = TrackedConfig() + cfg.name = "changed" + assert cfg.is_dirty + assert "name" in cfg.dirty_fields + + def test_not_dirty_when_set_to_same_value(self): + cfg = TrackedConfig(name="same") + cfg.name = "same" + assert not cfg.is_dirty + + def test_multiple_fields_tracked(self): + cfg = TrackedConfig() + cfg.name = "new" + cfg.count = 5 + assert cfg.dirty_fields == frozenset({"name", "count"}) + + def test_update_marks_fields_dirty(self): + cfg = TrackedConfig() + cfg.update({"name": "new", "count": 5}) + assert cfg.is_dirty + assert cfg.dirty_fields == frozenset({"name", "count"}) + + def test_dirty_fields_is_frozen(self): + cfg = TrackedConfig() + cfg.name = "x" + fs = cfg.dirty_fields + assert isinstance(fs, frozenset) + + +# ------------------------------------------------------------------ +# mark_clean +# ------------------------------------------------------------------ + + +class TestMarkClean: + def test_mark_clean_resets_dirty_fields(self): + cfg = TrackedConfig() + cfg.name = "x" + cfg.mark_clean() + assert not cfg.is_dirty + assert cfg.dirty_fields == frozenset() + + def test_mark_clean_resets_change_notes(self): + cfg = TrackedConfig() + cfg.name = "x" + cfg.record_change_note("name", "renamed") + cfg.mark_clean() + assert cfg.change_notes == [] + + def test_dirty_again_after_mark_clean(self): + cfg = TrackedConfig() + cfg.name = "x" + cfg.mark_clean() + cfg.count = 10 + assert cfg.is_dirty + assert cfg.dirty_fields == frozenset({"count"}) + + +# ------------------------------------------------------------------ +# Change notes +# ------------------------------------------------------------------ + + +class TestChangeNotes: + def test_no_notes_by_default(self): + cfg = TrackedConfig() + assert cfg.change_notes == [] + + def test_record_and_retrieve_note(self): + cfg = TrackedConfig() + cfg.name = "new" + cfg.record_change_note("name", "name was updated to 'new'") + assert cfg.change_notes == ["name was updated to 'new'"] + + def test_note_overwrites_previous_for_same_field(self): + cfg = TrackedConfig() + cfg.name = "a" + cfg.record_change_note("name", "first") + cfg.record_change_note("name", "second") + assert cfg.change_notes == ["second"] + + def test_multiple_notes_for_different_fields(self): + cfg = TrackedConfig() + cfg.name = "x" + cfg.count = 1 + cfg.record_change_note("name", "note-name") + cfg.record_change_note("count", "note-count") + assert set(cfg.change_notes) == {"note-name", "note-count"} + + def test_include_caller_appends_tag(self): + cfg = TrackedConfig() + cfg.name = "x" + cfg.record_change_note("name", "updated", include_caller=True) + notes = cfg.change_notes + assert len(notes) == 1 + assert notes[0].startswith("updated [") + assert "test_change_tracking.py:" in notes[0] + + +# ------------------------------------------------------------------ +# log_changes +# ------------------------------------------------------------------ + + +class TestLogChanges: + def test_log_changes_no_output_when_clean(self, caplog): + cfg = TrackedConfig() + with caplog.at_level(logging.INFO): + cfg.log_changes() + assert caplog.text == "" + + def test_log_changes_includes_note(self, caplog): + cfg = TrackedConfig() + cfg.name = "x" + cfg.record_change_note("name", "name was updated") + with caplog.at_level(logging.INFO): + cfg.log_changes() + assert "name was updated" in caplog.text + + def test_log_changes_shows_field_without_note(self, caplog): + cfg = TrackedConfig() + cfg.count = 99 + with caplog.at_level(logging.INFO): + cfg.log_changes() + assert "count was modified" in caplog.text + + def test_log_changes_mixes_notes_and_bare_fields(self, caplog): + cfg = TrackedConfig() + cfg.name = "y" + cfg.count = 5 + cfg.record_change_note("name", "renamed to y") + with caplog.at_level(logging.INFO): + cfg.log_changes() + assert "renamed to y" in caplog.text + assert "count was modified" in caplog.text + + def test_log_changes_header_contains_class_name(self, caplog): + cfg = TrackedConfig() + cfg.flag = True + with caplog.at_level(logging.INFO): + cfg.log_changes() + assert "TrackedConfig" in caplog.text + + +# ------------------------------------------------------------------ +# Integration with DLCVersionedConfig.from_yaml +# ------------------------------------------------------------------ + + +class TestFromYamlIntegration: + def test_clean_after_from_yaml(self, tmp_path): + path = tmp_path / "config.yaml" + path.write_text("name: loaded\ncount: 42\nflag: true\n") + cfg = TrackedConfig.from_yaml(path) + assert cfg.name == "loaded" + assert not cfg.is_dirty + assert cfg.change_notes == [] + + def test_roundtrip_stays_clean(self, tmp_path): + path = tmp_path / "config.yaml" + orig = TrackedConfig(name="rt", count=7) + orig.to_yaml(path) + loaded = TrackedConfig.from_yaml(path) + assert not loaded.is_dirty + + +# ------------------------------------------------------------------ +# Interaction with pydantic validate_assignment +# ------------------------------------------------------------------ + + +class TestValidateAssignment: + """Verify that change tracking wraps *around* pydantic's validator.""" + + def test_valid_assignment_is_tracked(self): + cfg = TrackedConfig() + cfg.count = 7 + assert cfg.count == 7 + assert "count" in cfg.dirty_fields + + def test_invalid_assignment_rejected_and_not_tracked(self): + cfg = TrackedConfig() + with pytest.raises(ValidationError): + cfg.count = "not-an-int" + assert cfg.count == 0 + assert not cfg.is_dirty + + def test_coerced_value_stored(self): + """Pydantic coerces compatible types (e.g. bool -> int); the coerced + value should be stored and tracked.""" + cfg = TrackedConfig() + cfg.count = True # coerced to 1 + assert cfg.count == 1 + assert "count" in cfg.dirty_fields + + +# ------------------------------------------------------------------ +# Instance isolation — two instances of the same class are independent +# ------------------------------------------------------------------ + + +class TestInstanceIsolation: + """The class-level __setattr__ patch must not cause instances to share state.""" + + def test_two_instances_independent_dirty_sets(self): + cfg1 = TrackedConfig() + cfg2 = TrackedConfig() + cfg1.name = "one" + assert "name" in cfg1.dirty_fields + assert not cfg2.is_dirty + + def test_mark_clean_on_one_does_not_affect_other(self): + cfg1 = TrackedConfig() + cfg2 = TrackedConfig() + cfg1.name = "x" + cfg2.count = 5 + cfg1.mark_clean() + assert not cfg1.is_dirty + assert cfg2.is_dirty + assert "count" in cfg2.dirty_fields + + def test_different_subclasses_independent(self): + class ConfigA(DLCVersionedConfig): + x: int = 0 + + class ConfigB(DLCVersionedConfig): + y: int = 0 + + a = ConfigA() + b = ConfigB() + a.x = 1 + assert "x" in a.dirty_fields + assert not b.is_dirty + + +# ------------------------------------------------------------------ +# from_dict and to_yaml lifecycle +# ------------------------------------------------------------------ + + +class TestLifecycle: + def test_from_dict_is_clean(self): + cfg = TrackedConfig.from_dict({"name": "loaded", "count": 5}) + assert not cfg.is_dirty + assert cfg.change_notes == [] + + def test_to_yaml_marks_writer_clean(self, tmp_path): + path = tmp_path / "out.yaml" + cfg = TrackedConfig(name="dirty") + cfg.name = "changed" + assert cfg.is_dirty + cfg.to_yaml(path) + assert not cfg.is_dirty + + def test_to_yaml_with_mark_clean_false_does_not_reset(self, tmp_path): + path = tmp_path / "out.yaml" + cfg = TrackedConfig() + cfg.name = "changed" + cfg.to_yaml(path, mark_clean=False) + assert cfg.is_dirty + + def test_record_change_note_for_unmodified_field_is_stored(self): + """record_change_note does not require the field to be dirty; it just stores the note.""" + cfg = TrackedConfig() + cfg.record_change_note("name", "set during init") + assert cfg.change_notes == ["set during init"] + assert not cfg.is_dirty # still clean — no assignment was made + + +# ------------------------------------------------------------------ +# Alias access with change tracking +# ------------------------------------------------------------------ + + +class TrackedAliasConfig(DLCVersionedConfig): + project_path: str = Field( + default="", + json_schema_extra={"aliases": ["projectPath"]}, + ) + + +class TestAliasChangeTracking: + def test_setitem_alias_marks_canonical_field_dirty(self): + cfg = TrackedAliasConfig() + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + cfg["projectPath"] = "/new" + assert cfg.project_path == "/new" + assert "project_path" in cfg.dirty_fields + + def test_setattr_alias_marks_canonical_field_dirty(self): + cfg = TrackedAliasConfig() + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + cfg.projectPath = "/attr" + assert cfg.project_path == "/attr" + assert "project_path" in cfg.dirty_fields diff --git a/tests/core/config/test_config_breakage.py b/tests/core/config/test_config_breakage.py new file mode 100644 index 0000000000..af85cc2724 --- /dev/null +++ b/tests/core/config/test_config_breakage.py @@ -0,0 +1,282 @@ +"""Pathological casess tests for the centralized config model.""" + +from __future__ import annotations + +import enum +import logging +from pathlib import Path + +import pytest +from pydantic import Field, ValidationError + +from deeplabcut.core.config import DLCBaseConfig, DLCVersionedConfig, ProjectConfig, versioning +from deeplabcut.core.deprecation import DLCDeprecationWarning + +# ----------------------------------------------------------------------------- +# In-place nested mutation +# ----------------------------------------------------------------------------- + + +@pytest.mark.xfail( + reason="This may make validation difficult and cause subtle issues " + "if we start changing how functions such as append() operate." + " Avoiding lists in configs could be a better long-term solution." +) +def test_in_place_list_mutation_should_be_validated_like_assignment(): + """Appending to a config list changes config state without calling __setattr__. + + A strict config system should reject invalid values even when users mutate an + existing list instead of assigning a replacement list. + + Note that this pattern would incur validation: + cfg.TrainingFraction = [*cfg.TrainingFraction, 0.8] + cfg.video_sets = {**cfg.video_sets, "video.mp4": {"crop": "0, 100, 0, 100"}} + """ + cfg = ProjectConfig(TrainingFraction=[0.95]) + cfg.mark_clean() + + with pytest.raises(ValidationError): + cfg.TrainingFraction.append(1.5) + + +@pytest.mark.xfail(reason="Not implemented yet") +@pytest.mark.parametrize( + ("field_name", "mutate"), + [ + ("TrainingFraction", lambda cfg: cfg.TrainingFraction.append(0.8)), + ("video_sets", lambda cfg: cfg.video_sets.__setitem__("video.mp4", {"crop": "0, 100, 0, 100"})), + ], +) +def test_in_place_nested_mutation_should_mark_config_dirty(field_name, mutate): + """Mutating nested containers changes the saved config but may bypass dirty tracking. + + Dirty tracking is useful only if semantic config changes are recorded, whether + the user assigns a whole field or mutates a nested list/dict in place. + This ties with the problem that some nested fields are still dict instead of config models, + maybe once we have full config models we can come up with a parent/child system where changes in a child + are detected when querying the parent for its dirty state (or a simpler equivalent) + """ + cfg = ProjectConfig(TrainingFraction=[0.95], video_sets={}) + cfg.mark_clean() + + mutate(cfg) + + assert cfg.is_dirty + assert field_name in cfg.dirty_fields + + +# ----------------------------------------------------------------------------- +# Private state isolation +# ----------------------------------------------------------------------------- + + +class _TrackedForIsolation(DLCVersionedConfig): + name: str = "default" + count: int = 0 + + +def test_dirty_state_is_not_shared_between_instances(): + """Private dirty-field state must be per-instance, not shared via a mutable default.""" + first = _TrackedForIsolation() + second = _TrackedForIsolation() + + first.name = "changed" + + assert "name" in first.dirty_fields + assert not second.is_dirty + assert "name" not in second.dirty_fields + + +def test_change_notes_are_not_shared_between_instances(): + """Private change-note state must be per-instance, not shared via a mutable default.""" + first = _TrackedForIsolation() + second = _TrackedForIsolation() + + first.record_change_note("name", "name changed on first instance") + + assert first.change_notes == ["name changed on first instance"] + assert second.change_notes == [] + + +# ----------------------------------------------------------------------------- +# Changes note consistency +# ----------------------------------------------------------------------------- + + +def test_change_note_should_reject_unknown_field_names(): + """Notes for misspelled fields are silently lost during logging. + + Rejecting unknown field names catches typos at the call site instead of + storing a note that can never match a dirty field. + """ + cfg = ProjectConfig() + + with pytest.raises(KeyError): + cfg.record_change_note("not_a_real_field", "this note should not be accepted") + + +def test_change_note_recorded_with_alias_should_follow_canonical_dirty_field(caplog): + """Aliases should not split notes from the canonical dirty field name. + + If a note is recorded with a deprecated alias, log_changes should still use + that note when the canonical field is modified. + """ + cfg = ProjectConfig() + cfg.mark_clean() + + with pytest.warns(DLCDeprecationWarning, match="with_identity"): + cfg.record_change_note("with_identity", "identity changed through compatibility alias") + cfg.identity = True + + with caplog.at_level(logging.INFO): + cfg.log_changes() + + assert "identity changed through compatibility alias" in caplog.text + + +# ----------------------------------------------------------------------------- +# Nested YAML comments and nested serialization +# ----------------------------------------------------------------------------- + + +class _CommentedInner(DLCBaseConfig): + threshold: float = Field( + default=0.5, + json_schema_extra={"comment": "Nested threshold comment"}, + ) + + +class _CommentedOuter(DLCBaseConfig): + inner: _CommentedInner = Field( + default_factory=_CommentedInner, + json_schema_extra={"comment": "Inner config section"}, + ) + + +def test_to_yaml_should_emit_comments_for_nested_config_fields(tmp_path): + """Nested config models can have field comments too. + + This protects against only applying YAML comments to the top-level model and + silently dropping useful nested schema documentation. + """ + path = tmp_path / "commented.yaml" + + _CommentedOuter().to_yaml(path) + + text = path.read_text() + assert "Inner config section" in text + assert "Nested threshold comment" in text + + +class _SerializationMode(enum.Enum): + FAST = "fast" + ACCURATE = "accurate" + + +class _SerializableInner(DLCBaseConfig): + output_path: Path = Path("outputs/predictions") + mode: _SerializationMode = _SerializationMode.FAST + + +class _SerializableOuter(DLCBaseConfig): + inner: _SerializableInner = Field(default_factory=_SerializableInner) + + +def test_nested_config_normalization_should_handle_paths_and_enums(): + """Nested model dumps should normalize non-primitive values before YAML output. + + Path and Enum values are common in configs and should become plain YAML-safe + values even when they appear inside nested config models. + """ + cfg = _SerializableOuter() + + assert cfg.to_dict(normalize=True) == { + "inner": { + "output_path": str(Path("outputs/predictions")), + "mode": "fast", + } + } + + +def test_nested_config_to_yaml_should_write_plain_path_and_enum_values(tmp_path): + """YAML output should not rely on Python-specific object representation. + + This catches failures where nested BaseModel values, pathlib paths, or enums + are passed to the YAML dumper without normalization. + """ + path = tmp_path / "serializable.yaml" + + _SerializableOuter().to_yaml(path) + + text = path.read_text() + assert str(Path("outputs/predictions")) in text + assert "fast" in text + + +# ----------------------------------------------------------------------------- +# Alias warnings and canonical writes +# ----------------------------------------------------------------------------- + + +def test_item_assignment_with_alias_should_warn_once_and_track_canonical_field(): + """Dict-style alias assignment should not warn twice or track the alias name. + + __setitem__ resolves aliases before delegating to assignment validation, so + the visible side effects should be one warning and one canonical dirty field. + """ + cfg = ProjectConfig() + cfg.mark_clean() + + with pytest.warns(DLCDeprecationWarning, match="with_identity") as caught: + cfg["with_identity"] = True + + assert len(caught) == 1 + assert cfg.identity is True + assert "identity" in cfg.dirty_fields + assert "with_identity" not in cfg.dirty_fields + + +# ----------------------------------------------------------------------------- +# Cross-field validation vs bulk update +# ----------------------------------------------------------------------------- + + +@pytest.mark.xfail( + reason=( + "update() applies overrides one setattr at a time; cross-field " + "model validators run on each assignment, so intermediate states " + "like multianimalproject=True with bodyparts=[] are rejected." + ), +) +def test_update_applies_cross_field_overrides_atomically(): + """Bulk update should validate the merged state, not each setattr in isolation.""" + cfg = ProjectConfig(bodyparts=[], multianimalproject=False) + # Invalid state, midway updating: when bodyparts is set to "MULTI!" before multianimalproject is set to True. + cfg.update({"bodyparts": "MULTI!", "multianimalproject": True}) + assert cfg.multianimalproject is True + assert cfg.bodyparts == "MULTI!" + + +# ----------------------------------------------------------------------------- +# Migration skipped on typed construction +# ----------------------------------------------------------------------------- + + +class _TypedConstructionCfg(DLCVersionedConfig): + config_version: int = 1 + epochs: int = 50 + + +@pytest.mark.parametrize("kwargs", [{}, {"epochs": 100}]) +def test_migration_skipped_on_typed_construction(monkeypatch, kwargs): + """Typed construction must not run the legacy YAML migration chain. + + ``MyCfg(epochs=100)`` is current-schema construction, not loading an old file. + Migration should only run via ``from_dict`` / ``from_yaml``. + """ + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", 1) + + cfg = _TypedConstructionCfg(**kwargs) + + assert cfg.epochs == kwargs.get("epochs", 50) + assert cfg.config_version == 1 diff --git a/tests/core/config/test_core_config.py b/tests/core/config/test_core_config.py new file mode 100644 index 0000000000..a5422afa6d --- /dev/null +++ b/tests/core/config/test_core_config.py @@ -0,0 +1,482 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for deeplabcut.core.config.""" + +import logging +from collections.abc import Mapping +from pathlib import Path + +import pytest +from pydantic import ValidationError +from ruamel.yaml.error import YAMLError + +from deeplabcut.core.config import ( + create_config_template, + create_config_template_3d, + edit_config, + pretty_print, + read_config, + read_config_as_dict, + resolve_alias, + resolve_aliases_in_dict, + write_config, + write_config_3d, + write_config_3d_template, + write_project_config, +) +from deeplabcut.core.deprecation import DLCDeprecationWarning + +# ----------------------------------------------------------------------------- +# read_config_as_dict +# ----------------------------------------------------------------------------- + + +def test_read_config_as_dict_loads_yaml(tmp_path): + config_path = tmp_path / "config.yaml" + config_path.write_text("a: 1\nb: [2, 3]\nc:\n d: 4\n") + cfg = read_config_as_dict(str(config_path)) + assert cfg == {"a": 1, "b": [2, 3], "c": {"d": 4}} + + +def test_read_config_as_dict_raises_when_file_missing(): + with pytest.raises(FileNotFoundError): + read_config_as_dict(Path("/nonexistent/config.yaml")) + + +def test_read_config_as_dict_raises_on_invalid_yaml(tmp_path): + """Broken YAML syntax (e.g. unclosed bracket) raises an exception.""" + config_path = tmp_path / "broken.yaml" + config_path.write_text("key: [unclosed\n") + with pytest.raises(YAMLError): + read_config_as_dict(config_path) + + +@pytest.mark.parametrize("content", ["", "null", "~"]) +def test_read_config_as_dict_raises_when_empty_or_null(tmp_path, content): + config_path = tmp_path / "config.yaml" + config_path.write_text(content) + with pytest.raises(ValueError, match="empty or null"): + read_config_as_dict(config_path) + + +def test_read_config_as_dict_raises_when_root_is_not_mapping(tmp_path): + config_path = tmp_path / "config.yaml" + config_path.write_text("- item\n") + with pytest.raises(ValueError, match="must be a YAML mapping"): + read_config_as_dict(config_path) + + +def test_read_config_as_dict_breaks_for_yaml_tags(tmp_path): + """read_config breaks for YAML tags like !!python/tuple""" + config_path = tmp_path / "config.yaml" + config_path.write_text("project_path: /old/path\nengine: pytorch\nbodyparts: !!python/tuple [a, b, c]\n") + with pytest.raises(YAMLError): + read_config_as_dict(config_path) + + +def test_read_config_as_dict_accepts_config_with_misnamed_fields(tmp_path): + """Config with typos/misnamed keys still loads; keys are not validated.""" + # NOTE @deruyter92 2026-02-03: This test captures the current behavior where + # read_config_as_dict does not validate the keys. This behavior is different + # from read_config, which should validate the keys (in the future). + # The tests can be updated according, depending on the preferred behavior. + config_path = tmp_path / "typos.yaml" + config_path.write_text( + "project_pathh: /wrong\n" # typo + "bodypartz: [a, b]\n" # typo + "Task: mytask\n" + ) + cfg = read_config_as_dict(config_path) + assert cfg["project_pathh"] == "/wrong" + assert cfg["bodypartz"] == ["a", "b"] + assert cfg["Task"] == "mytask" + assert "project_path" not in cfg + assert "bodyparts" not in cfg + + +# ----------------------------------------------------------------------------- +# write_config +# ----------------------------------------------------------------------------- + + +def test_write_config_creates_file(tmp_path): + config_path = tmp_path / "out.yaml" + write_config(config_path, {"a": 1, "b": 2}) + assert config_path.exists() + cfg = read_config_as_dict(config_path) + assert cfg["a"] == 1 and cfg["b"] == 2 + + +def test_write_config_overwrites_existing(tmp_path): + config_path = tmp_path / "out.yaml" + config_path.write_text("old: true\n") + write_config(config_path, {"new": True}, overwrite=True) + cfg = read_config_as_dict(config_path) + assert "new" in cfg and "old" not in cfg + + +def test_write_config_raises_when_overwrite_false_and_file_exists(tmp_path): + config_path = tmp_path / "out.yaml" + config_path.write_text("x: 1\n") + with pytest.raises(FileExistsError): + write_config(config_path, {"x": 2}, overwrite=False) + + +def test_write_config_allows_overwrite_false_when_file_missing(tmp_path): + config_path = tmp_path / "new.yaml" + write_config(config_path, {"x": 1}, overwrite=False) + assert read_config_as_dict(config_path) == {"x": 1} + + +# ----------------------------------------------------------------------------- +# pretty_print +# ----------------------------------------------------------------------------- + + +def test_pretty_print_flat_config(capsys): + pretty_print({"a": 1, "b": 2}) + out, _ = capsys.readouterr() + assert "a: 1" in out and "b: 2" in out + + +def test_pretty_print_nested_config(capsys): + pretty_print({"top": {"nested": 42}}) + out, _ = capsys.readouterr() + assert "top:" in out and "nested: 42" in out + + +def test_pretty_print_with_indent(capsys): + pretty_print({"k": "v"}, indent=4) + out, _ = capsys.readouterr() + assert out.startswith(" k:") + + +def test_pretty_print_with_custom_print_fn(): + lines = [] + + def capture(s): + lines.append(s) + + pretty_print({"a": 1}, print_fn=capture) + assert any("a: 1" in line for line in lines) + + +# ----------------------------------------------------------------------------- +# create_config_template +# ----------------------------------------------------------------------------- + + +@pytest.mark.parametrize("multianimal", [False, True]) +def test_create_config_template_returns_tuple(multianimal): + cfg_file, ruamel_file = create_config_template(multianimal=multianimal) + assert isinstance(cfg_file, Mapping) + assert ruamel_file is not None + + +def test_create_config_template_single_animal_has_expected_keys(): + cfg_file, _ = create_config_template(multianimal=False) + assert "Task" in cfg_file + assert "project_path" in cfg_file + assert "bodyparts" in cfg_file + assert "engine" in cfg_file + assert "video_sets" in cfg_file + assert "multianimalproject" in cfg_file + assert "detector_batch_size" in cfg_file + + +def test_create_config_template_multianimal_has_extra_keys(): + cfg_file, _ = create_config_template(multianimal=True) + assert "individuals" in cfg_file + assert "uniquebodyparts" in cfg_file + assert "multianimalbodyparts" in cfg_file + assert "bodyparts" in cfg_file + + +# ----------------------------------------------------------------------------- +# create_config_template_3d +# ----------------------------------------------------------------------------- + + +def test_create_config_template_3d_returns_tuple(): + cfg_file_3d, ruamel_file_3d = create_config_template_3d() + assert isinstance(cfg_file_3d, Mapping) + assert ruamel_file_3d is not None + + +def test_create_config_template_3d_has_expected_keys(): + cfg_file, _ = create_config_template_3d() + assert "Task" in cfg_file + assert "project_path" in cfg_file + assert "skeleton" in cfg_file + assert "num_cameras" in cfg_file + assert "camera_names" in cfg_file + assert "scorername_3d" in cfg_file + + +# ----------------------------------------------------------------------------- +# read_config +# ----------------------------------------------------------------------------- + + +def test_read_config_raises_when_file_missing(tmp_path): + with pytest.raises(FileNotFoundError): + read_config(tmp_path / "missing.yaml") + + +def test_read_config_sets_missing_engine_and_writes_back(tmp_path): + config_path = tmp_path / "config.yaml" + config_path.write_text("project_path: /other/path\n") + cfg = read_config(config_path) + assert cfg["engine"] == "pytorch" + assert cfg["project_path"] == tmp_path + # File should have been updated + cfg_again = read_config_as_dict(config_path) + assert cfg_again["engine"] == "pytorch" + + +def test_read_config_sets_detector_snapshotindex_when_missing(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, {"project_path": str(tmp_path), "engine": "pytorch"}) + cfg = read_config(config_path) + assert cfg["detector_snapshotindex"] == -1 + + +def test_read_config_sets_detector_batch_size_when_missing(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, {"project_path": str(tmp_path), "engine": "pytorch"}) + cfg = read_config(config_path) + assert cfg["detector_batch_size"] == 1 + + +def test_read_config_updates_project_path_when_different(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config( + config_path, + {"project_path": "/old/path", "engine": "pytorch"}, + ) + cfg = read_config(config_path) + assert cfg["project_path"] == tmp_path + + +@pytest.mark.parametrize("engine", ["pytorch", "tensorflow"]) +def test_read_config_preserves_existing_engine_and_project_path(tmp_path, engine): + config_path = tmp_path / "config.yaml" + write_project_config( + config_path, + {"project_path": str(tmp_path), "engine": engine}, + ) + cfg = read_config(config_path) + assert cfg["engine"] == engine + assert cfg["project_path"] == tmp_path + + +def test_read_config_breaks_for_yaml_tags(tmp_path): + """read_config raises for YAML files containing unsafe tags like !!python/tuple.""" + config_path = tmp_path / "config.yaml" + config_path.write_text("project_path: /old/path\nengine: pytorch\nbodyparts: !!python/tuple [a, b, c]\n") + with pytest.raises(YAMLError): + read_config(config_path) + + +def test_read_config_breaks_for_invalid_fields(tmp_path): + """read_config raises when the YAML contains field names not declared by ProjectConfig.""" + config_path = tmp_path / "typos.yaml" + config_path.write_text( + "project_pathh: /wrong\n" # typo + "bodypartz: [a, b]\n" # typo + "Task: mytask\n" + ) + with pytest.raises(ValidationError): + read_config(config_path) + + +# ----------------------------------------------------------------------------- +# write_project_config +# ----------------------------------------------------------------------------- + + +def test_write_project_config_writes_valid_yaml(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config( + config_path, + {"project_path": str(tmp_path), "Task": "mytask", "bodyparts": ["a", "b"]}, + ) + cfg = read_config_as_dict(config_path) + assert cfg["project_path"] == str(tmp_path) + assert cfg["Task"] == "mytask" + assert cfg["bodyparts"] == ["a", "b"] + + +def test_write_project_config_adds_skeleton_defaults_when_missing(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config( + config_path, + {"project_path": str(tmp_path), "multianimalproject": False}, + ) + cfg = read_config_as_dict(config_path) + assert cfg["skeleton"] == [] + assert cfg["skeleton_color"] == "black" + + +def test_write_project_config_uses_multianimal_template_when_flag_true(tmp_path): + config_path = tmp_path / "config.yaml" + write_project_config( + config_path, + {"project_path": str(tmp_path), "multianimalproject": True}, + ) + cfg = read_config_as_dict(config_path) + assert "individuals" in cfg + assert "uniquebodyparts" in cfg + + +def test_write_project_config_falls_back_and_preserves_unknown_keys(tmp_path, caplog): + """Unvalidated legacy write succeeds via fallback with logging, but may not round-trip via read_config.""" + config_path = tmp_path / "config.yaml" + payload = { + "project_path": str(tmp_path), + "Task": "mytask", + "bodyparts": ["a", "b"], + "legacy_custom_field": "keep-me", # extra="forbid" on ProjectConfig, will be preserved + } + + with caplog.at_level(logging.ERROR, logger="deeplabcut.core.config.utils"): + with pytest.warns(UserWarning, match="legacy config file writing"): + write_project_config(config_path, payload) + + assert "Invalid configuration" in caplog.text + assert config_path.is_file() + written = read_config_as_dict(config_path) + assert written["legacy_custom_field"] == "keep-me" + assert written["Task"] == "mytask" + + with pytest.raises(ValidationError): + read_config(config_path) + + +# ----------------------------------------------------------------------------- +# edit_config +# ----------------------------------------------------------------------------- + + +def test_edit_config_applies_edits_and_overwrites_original(tmp_path): + config_path = tmp_path / "config.yaml" + config_path.write_text("a: 1\nb: 2\n") + cfg = edit_config(config_path, {"b": 99, "c": 3}) + assert cfg["a"] == 1 and cfg["b"] == 99 and cfg["c"] == 3 + loaded = read_config_as_dict(config_path) + assert loaded["b"] == 99 and loaded["c"] == 3 + + +def test_edit_config_writes_to_output_name_when_given(tmp_path): + src = tmp_path / "src.yaml" + src.write_text("x: 1\n") + out = tmp_path / "out.yaml" + cfg = edit_config(src, {"x": 2}, output_name=out) + assert cfg["x"] == 2 + assert read_config_as_dict(out)["x"] == 2 + assert read_config_as_dict(src)["x"] == 1 + + +# ----------------------------------------------------------------------------- +# write_config_3d +# ----------------------------------------------------------------------------- + + +def test_write_config_3d_writes_valid_yaml(tmp_path): + config_path = tmp_path / "config_3d.yaml" + write_config_3d( + config_path, + {"project_path": str(tmp_path), "Task": "3dtask", "num_cameras": 2}, + ) + cfg = read_config_as_dict(config_path) + assert cfg["project_path"] == str(tmp_path) + assert cfg["Task"] == "3dtask" + assert cfg["num_cameras"] == 2 + + +# ----------------------------------------------------------------------------- +# write_config_3d_template +# ----------------------------------------------------------------------------- + + +def test_write_config_3d_template_writes_given_template(tmp_path): + config_path = tmp_path / "config_3d.yaml" + cfg_file, ruamel_file = create_config_template_3d() + cfg_file["Task"] = "custom_3d" + cfg_file["num_cameras"] = 3 + write_config_3d_template(config_path, cfg_file, ruamel_file) + cfg = read_config_as_dict(config_path) + assert cfg["Task"] == "custom_3d" + assert cfg["num_cameras"] == 3 + + +# ----------------------------------------------------------------------------- +# resolve_alias / resolve_aliases_in_dict +# ----------------------------------------------------------------------------- + + +ALIAS_MAP = {"projectPath": "project_path"} + + +def test_resolve_alias_returns_canonical_for_alias(): + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + assert resolve_alias("projectPath", ALIAS_MAP) == "project_path" + + +def test_resolve_alias_returns_name_unchanged_for_unknown_key(): + assert resolve_alias("Task", ALIAS_MAP, warn=False) == "Task" + + +def test_resolve_alias_no_warning_when_warn_false(): + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + assert resolve_alias("projectPath", ALIAS_MAP, warn=False) == "project_path" + + +def test_resolve_aliases_in_dict_resolves_single_alias(): + with pytest.warns(DLCDeprecationWarning, match="projectPath"): + resolved = resolve_aliases_in_dict( + {"Task": "t", "projectPath": "/p"}, + ALIAS_MAP, + target="ToyConfig", + ) + assert resolved == {"Task": "t", "project_path": "/p"} + + +def test_resolve_aliases_in_dict_empty_alias_map_returns_input_unchanged(): + cfg_dict = {"Task": "t", "project_path": "/p"} + assert resolve_aliases_in_dict(cfg_dict, {}) is cfg_dict + + +def test_resolve_aliases_in_dict_rejects_alias_and_canonical_together(): + with pytest.raises(TypeError, match=r"projectPath.*project_path"): + resolve_aliases_in_dict( + {"projectPath": "/alias", "project_path": "/canonical"}, + ALIAS_MAP, + target="ToyConfig", + warn=False, + ) + + +def test_resolve_aliases_in_dict_rejects_two_aliases_for_same_field(): + alias_map = { + "projectPath": "project_path", + "legacyProjectPath": "project_path", + } + with pytest.raises(TypeError, match=r"projectPath.*legacyProjectPath"): + resolve_aliases_in_dict( + {"projectPath": "/a", "legacyProjectPath": "/b"}, + alias_map, + target="ToyConfig", + warn=False, + ) diff --git a/tests/core/config/test_project_config.py b/tests/core/config/test_project_config.py new file mode 100644 index 0000000000..249266d522 --- /dev/null +++ b/tests/core/config/test_project_config.py @@ -0,0 +1,238 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for ProjectConfig — the real DLCVersionedConfig consumer.""" + +import logging +from pathlib import Path + +import pytest + +from deeplabcut.core.config import ProjectConfig, read_config_as_dict +from deeplabcut.core.config.versioning import CURRENT_CONFIG_VERSION + +# ----------------------------------------------------------------------------- +# Basic construction / defaults +# ----------------------------------------------------------------------------- + + +class TestProjectConfigDefaults: + def test_defaults(self): + cfg = ProjectConfig() + assert cfg.config_version == CURRENT_CONFIG_VERSION + assert cfg.engine == "pytorch" + assert cfg.Task == "" + assert isinstance(cfg.project_path, Path) + assert cfg.pose_config_path is None + assert cfg.config_yaml_path == cfg.project_path / "config.yaml" + assert cfg.config_yaml_path == cfg.project_path / "config.yaml" + assert cfg.config_yaml_path == cfg.project_path / "config.yaml" + + def test_from_dict_minimal(self): + cfg = ProjectConfig.from_dict( + { + "Task": "mytask", + "scorer": "scorer", + "date": "Jan01", + "project_path": "/tmp/project", + } + ) + assert cfg.Task == "mytask" + assert cfg.project_path == Path("/tmp/project") + + def test_from_dict_rejects_unknown_keys(self): + from pydantic import ValidationError + + with pytest.raises(ValidationError): + ProjectConfig.from_dict({"Task": "t", "not_a_real_field": 1}) + + def test_is_versioned_config(self): + cfg = ProjectConfig() + assert hasattr(cfg, "is_dirty") + assert not cfg.is_dirty + + +# ----------------------------------------------------------------------------- +# bodyparts_list +# ----------------------------------------------------------------------------- + + +class TestProjectConfigBodypartsList: + def test_single_animal_bodyparts_list_returns_bodyparts(self): + cfg = ProjectConfig(multianimalproject=False, bodyparts=["nose", "tail"]) + assert cfg.bodyparts_list == ["nose", "tail"] + + def test_multi_animal_bodyparts_list_returns_multianimalbodyparts(self): + cfg = ProjectConfig( + multianimalproject=True, + bodyparts="MULTI!", + multianimalbodyparts=["nose", "tail"], + ) + assert cfg.bodyparts_list == ["nose", "tail"] + + def test_single_animal_multi_sentinel_raises(self): + from pydantic import ValidationError + + with pytest.raises(ValidationError, match="MULTI!"): + ProjectConfig(multianimalproject=False, bodyparts="MULTI!") + + +# ----------------------------------------------------------------------------- +# validate_project_path / from_any(repair_path=True) +# ----------------------------------------------------------------------------- + + +def _write_wrong_project_path_yaml(config_path: Path) -> None: + config_path.write_text("project_path: /completely/wrong/path\nengine: pytorch\n") + + +class TestValidateProjectPath: + def test_raises_when_yaml_not_found(self, tmp_path): + cfg = ProjectConfig(project_path=tmp_path) + with pytest.raises(FileNotFoundError, match="config.yaml not found"): + cfg.validate_project_path() + + @pytest.mark.parametrize("write", [False, True]) + def test_validate_project_path_write_flag(self, tmp_path, write): + config_path = tmp_path / "config.yaml" + _write_wrong_project_path_yaml(config_path) + cfg = ProjectConfig(project_path=Path("/completely/wrong/path")) + cfg.validate_project_path(yaml_path=config_path, write=write) + assert cfg.project_path == tmp_path + saved = read_config_as_dict(config_path) + if write: + assert Path(saved["project_path"]) == tmp_path + assert not cfg.is_dirty + else: + assert saved["project_path"] == "/completely/wrong/path" + assert "project_path" in cfg.dirty_fields + + @pytest.mark.parametrize( + ("config_input", "repair_path"), + [ + ("yaml_path", True), + ("dict", True), + ("dict", False), + ], + ids=["yaml_path", "dict_repair", "dict_no_repair"], + ) + def test_from_any_repair_path(self, tmp_path, config_input, repair_path): + config_path = tmp_path / "config.yaml" + _write_wrong_project_path_yaml(config_path) + if config_input == "yaml_path": + cfg = ProjectConfig.from_any(config_path, repair_path=repair_path) + else: + cfg = ProjectConfig.from_any( + {"project_path": tmp_path, "engine": "pytorch"}, + repair_path=repair_path, + ) + assert cfg.project_path == tmp_path + assert not cfg.is_dirty + saved = read_config_as_dict(config_path) + if config_input == "yaml_path" and repair_path: + assert Path(saved["project_path"]) == tmp_path + else: + assert saved["project_path"] == "/completely/wrong/path" + + +# ----------------------------------------------------------------------------- +# _post_yaml_load_updates: project_path repair +# ----------------------------------------------------------------------------- + + +class TestPostYamlLoadUpdates: + def test_project_path_updated_when_differs_from_yaml_location(self, tmp_path): + """When the stored project_path differs from the YAML's directory, + _post_yaml_load_updates corrects it and marks the field dirty.""" + config_path = tmp_path / "config.yaml" + config_path.write_text("project_path: /completely/wrong/path\nengine: pytorch\n") + cfg = ProjectConfig.from_yaml(config_path) + assert cfg.project_path == tmp_path + assert "project_path" in cfg.dirty_fields + assert any("project_path updated" in n for n in cfg.change_notes) + + def test_project_path_not_updated_when_already_correct(self, tmp_path): + """When the stored project_path already matches the YAML directory, + nothing is changed and the config stays clean.""" + config_path = tmp_path / "config.yaml" + config_path.write_text(f"project_path: {tmp_path}\nengine: pytorch\n") + cfg = ProjectConfig.from_yaml(config_path) + assert cfg.project_path == tmp_path + assert not cfg.is_dirty + + +# ----------------------------------------------------------------------------- +# YAML round-trip +# ----------------------------------------------------------------------------- + + +class TestProjectConfigYamlRoundtrip: + def test_roundtrip_preserves_scalar_fields(self, tmp_path): + config_path = tmp_path / "config.yaml" + orig = ProjectConfig( + Task="pose", + scorer="scorer1", + date="2026-01-01", + project_path=tmp_path, + engine="pytorch", + pcutoff=0.6, + ) + orig.to_yaml(config_path) + loaded = ProjectConfig.from_yaml(config_path) + assert loaded.Task == "pose" + assert loaded.scorer == "scorer1" + assert loaded.pcutoff == 0.6 + assert loaded.engine == "pytorch" + + def test_roundtrip_config_is_clean(self, tmp_path): + """After saving and reloading, the loaded config must be clean + (project_path already matches YAML location).""" + config_path = tmp_path / "config.yaml" + orig = ProjectConfig(project_path=tmp_path) + orig.to_yaml(config_path) + loaded = ProjectConfig.from_yaml(config_path) + assert not loaded.is_dirty + + def test_roundtrip_no_unknown_keys_in_yaml(self, tmp_path): + """The saved YAML must not contain keys that ProjectConfig doesn't declare.""" + config_path = tmp_path / "config.yaml" + ProjectConfig(project_path=tmp_path).to_yaml(config_path) + saved = read_config_as_dict(config_path) + declared = set(ProjectConfig.model_fields.keys()) + unknown = set(saved.keys()) - declared + assert unknown == set(), f"Unknown keys written to YAML: {unknown}" + + def test_to_yaml_writes_config_version(self, tmp_path): + config_path = tmp_path / "config.yaml" + ProjectConfig(project_path=tmp_path).to_yaml(config_path) + saved = read_config_as_dict(config_path) + assert "config_version" in saved + assert saved["config_version"] == CURRENT_CONFIG_VERSION + + +# ----------------------------------------------------------------------------- +# Migration / versioning integration +# ----------------------------------------------------------------------------- + + +class TestProjectConfigVersioning: + def test_no_migration_log_for_current_version_yaml(self, tmp_path, caplog): + """Loading a config already at CURRENT_CONFIG_VERSION emits no migration INFO log.""" + config_path = tmp_path / "config.yaml" + config_path.write_text(f"config_version: {CURRENT_CONFIG_VERSION}\nproject_path: {tmp_path}\nengine: pytorch\n") + with caplog.at_level(logging.INFO, logger="deeplabcut.core.config.versioning"): + ProjectConfig.from_yaml(config_path) + + migration_records = [r for r in caplog.records if "igrating" in r.message] + assert len(migration_records) == 0 + + def test_unversioned_yaml_treated_as_v0(self, tmp_path): + """A YAML file without config_version is treated as version 0.""" + config_path = tmp_path / "config.yaml" + config_path.write_text(f"project_path: {tmp_path}\nengine: pytorch\n") + cfg = ProjectConfig.from_yaml(config_path) + assert cfg.config_version == CURRENT_CONFIG_VERSION diff --git a/tests/core/config/test_versioning.py b/tests/core/config/test_versioning.py new file mode 100644 index 0000000000..12dbe95db0 --- /dev/null +++ b/tests/core/config/test_versioning.py @@ -0,0 +1,921 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for the configuration versioning and migration system.""" + +import logging + +import pytest +from pydantic import Field, ValidationError + +from deeplabcut.core.config import DLCVersionedConfig, versioning +from deeplabcut.core.config.versioning import ( + get_config_version, + migrate_config, + register_migration, +) +from deeplabcut.core.deprecation import DLCDeprecationWarning + +_LOGGER_NAME = "deeplabcut.core.config.versioning" + + +# ----------------------------------------------------------------------------- +# Toy migrations (registered only for tests; v98 <-> v99) +# ----------------------------------------------------------------------------- + +_TOY_VERSION_OLD = 98 +_TOY_VERSION_NEW = 99 + +_LEGACY_FIELD = "toy_legacy_field" +_NEW_FIELD = "toy_new_field" + + +_TOY_CONFIG_TYPE = "ToyVersionedConfig" + + +@pytest.fixture(autouse=True) +def _toy_versioned_config_migrations(register_toy_migrations): + """Default v98 <-> v99 migrations for ToyVersionedConfig tests in this module.""" + register_toy_migrations(_TOY_CONFIG_TYPE) + + +# ----------------------------------------------------------------------------- +# get_config_version +# ----------------------------------------------------------------------------- + + +def test_get_config_version_(): + assert get_config_version({}) == 0 + assert get_config_version({"Task": "mytask"}) == 0 + assert get_config_version({"config_version": 1}) == 1 + assert get_config_version({"config_version": 0}) == 0 + + +# ----------------------------------------------------------------------------- +# migrate_config (no-op, errors) +# ----------------------------------------------------------------------------- + + +def test_migrate_config_same_version_returns_unchanged(): + cfg = {"a": 1, "b": 2} + assert migrate_config(cfg, config_type=_TOY_CONFIG_TYPE, target_version=0) is cfg + + +def test_migrate_config_does_not_mutate_input(): + cfg = {"config_version": _TOY_VERSION_OLD, _LEGACY_FIELD: "x"} + result = migrate_config(cfg, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_NEW) + assert cfg[_LEGACY_FIELD] == "x" + assert result is not cfg + + +def test_migrate_config_target_exceeds_current_raises(monkeypatch): + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", 99) + with pytest.raises(ValueError, match="Target version .* exceeds current"): + migrate_config({"config_version": 0}, config_type=_TOY_CONFIG_TYPE, target_version=100) + + +# ----------------------------------------------------------------------------- +# Toy migration round-trip (v98 <-> v99) +# ----------------------------------------------------------------------------- + + +def test_migration_v98_to_v99_renames_toy_field(): + """Upgrade v98 -> v99: toy_legacy_field becomes toy_new_field.""" + v98 = {"config_version": _TOY_VERSION_OLD, _LEGACY_FIELD: "value", "other": 42} + v99 = migrate_config(v98, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_NEW) + assert get_config_version(v99) == _TOY_VERSION_NEW + assert _NEW_FIELD in v99 + assert v99[_NEW_FIELD] == "value" + assert _LEGACY_FIELD not in v99 + assert v99["other"] == 42 + + +def test_migration_v99_to_v98_renames_toy_field_back(): + """Downgrade v99 -> v98: toy_new_field becomes toy_legacy_field.""" + v99 = {"config_version": _TOY_VERSION_NEW, _NEW_FIELD: "value", "other": 42} + v98 = migrate_config(v99, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_OLD) + assert get_config_version(v98) == _TOY_VERSION_OLD + assert _LEGACY_FIELD in v98 + assert v98[_LEGACY_FIELD] == "value" + assert _NEW_FIELD not in v98 + assert v98["other"] == 42 + + +def test_roundtrip_v98_to_v99_to_v98_preserves_content(): + """Round-trip v98 -> v99 -> v98: content matches original v98.""" + original = { + "config_version": _TOY_VERSION_OLD, + _LEGACY_FIELD: "test_value", + "Task": "mytask", + "extra": [1, 2], + } + v99 = migrate_config(original, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_NEW) + back = migrate_config(v99, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_OLD) + assert get_config_version(back) == _TOY_VERSION_OLD + assert back[_LEGACY_FIELD] == original[_LEGACY_FIELD] + assert back["Task"] == original["Task"] + assert back["extra"] == original["extra"] + + +def test_roundtrip_v99_to_v98_to_v99_preserves_content(): + """Round-trip v99 -> v98 -> v99: content matches original v99.""" + original = { + "config_version": _TOY_VERSION_NEW, + _NEW_FIELD: "test_value", + "Task": "mytask", + "extra": [1, 2], + } + v98 = migrate_config(original, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_OLD) + back = migrate_config(v98, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_NEW) + assert get_config_version(back) == _TOY_VERSION_NEW + assert back[_NEW_FIELD] == original[_NEW_FIELD] + assert back["Task"] == original["Task"] + assert back["extra"] == original["extra"] + + +def test_roundtrip_v98_without_toy_field_unchanged(): + """Config without toy field is unchanged by v98 -> v99 -> v98.""" + original = { + "config_version": _TOY_VERSION_OLD, + "Task": "mytask", + "bodyparts": ["a", "b"], + } + v99 = migrate_config(original, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_NEW) + back = migrate_config(v99, config_type=_TOY_CONFIG_TYPE, target_version=_TOY_VERSION_OLD) + assert get_config_version(back) == _TOY_VERSION_OLD + assert back["Task"] == original["Task"] + assert back["bodyparts"] == original["bodyparts"] + + +# ----------------------------------------------------------------------------- +# DLCVersionedConfig: migration prevents validation error for renamed field +# ----------------------------------------------------------------------------- + + +@pytest.fixture +def ToyConfigWithValidField(): + class ToyConfig(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + valid_project_config_field: str = "" + + return ToyConfig + + +def test_config_with_legacy_field_raises_without_migration(monkeypatch, ToyConfigWithValidField): + """Legacy unknown field raises validation error when no migration is registered.""" + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", _TOY_VERSION_OLD) + config_with_legacy_field = { + "config_version": _TOY_VERSION_OLD, + "this_fieldname_is_not_in_project_config": "some_value", + } + with pytest.raises((ValidationError, TypeError)): + ToyConfigWithValidField(**config_with_legacy_field) + + +def test_config_after_migration_accepts_renamed_field(monkeypatch, ToyConfigWithValidField): + """Registering a migration renames the legacy field so the model accepts the config.""" + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", _TOY_VERSION_NEW) + config_with_legacy_field = { + "config_version": _TOY_VERSION_OLD, + "this_fieldname_is_not_in_project_config": "some_value", + } + + @register_migration(_TOY_VERSION_OLD, _TOY_VERSION_NEW, config_type="ToyConfig") + def _toy_migrate_legacy_to_valid_field(config: dict) -> dict: + """Test-only: rename legacy field -> valid_project_config_field.""" + if "this_fieldname_is_not_in_project_config" in config: + config["valid_project_config_field"] = config.pop("this_fieldname_is_not_in_project_config") + return config + + cfg = ToyConfigWithValidField.from_dict(config_with_legacy_field) + assert cfg.valid_project_config_field == "some_value" + + +# ----------------------------------------------------------------------------- +# Migration before alias resolution on DLCVersionedConfig +# ----------------------------------------------------------------------------- + + +class _MigrateThenAliasConfig(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + toy_new_field: str = Field( + default="", + json_schema_extra={"aliases": ["deprecated_alias"]}, + ) + + +@pytest.fixture +def _migrate_then_alias_current(register_toy_migrations): + register_toy_migrations("_MigrateThenAliasConfig") + + +def test_migration_then_alias_legacy_version_key(_migrate_then_alias_current): + """v98 legacy key is renamed by migration, then validated.""" + cfg = _MigrateThenAliasConfig.from_dict( + { + "config_version": _TOY_VERSION_OLD, + _LEGACY_FIELD: "from_migration", + } + ) + assert cfg.toy_new_field == "from_migration" + + +def test_migration_then_alias_same_version_deprecated_name(_migrate_then_alias_current): + """Same-version deprecated alias is resolved after migration (with a warning).""" + with pytest.warns(DLCDeprecationWarning, match="deprecated_alias"): + cfg = _MigrateThenAliasConfig.model_validate( + { + "config_version": _TOY_VERSION_NEW, + "deprecated_alias": "from_alias", + } + ) + assert cfg.toy_new_field == "from_alias" + + +def test_migration_then_alias_on_old_version_with_deprecated_alias( + _migrate_then_alias_current, +): + """Migrate to current version, then resolve alias when v98 used the alias key.""" + with pytest.warns(DLCDeprecationWarning, match="deprecated_alias"): + cfg = _MigrateThenAliasConfig.model_validate( + { + "config_version": _TOY_VERSION_OLD, + "deprecated_alias": "both_steps", + } + ) + assert cfg.toy_new_field == "both_steps" + + +def test_from_dict_runs_migration(_migrate_then_alias_current): + """Legacy dicts loaded via from_dict are migrated before validation.""" + + class _KwOnlyVersioned(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + toy_new_field: str = "" + + @register_migration(_TOY_VERSION_OLD, _TOY_VERSION_NEW, config_type="_KwOnlyVersioned") + def _kw_migrate_v98_to_v99(config: dict) -> dict: + if _LEGACY_FIELD in config: + config[_NEW_FIELD] = config.pop(_LEGACY_FIELD) + return config + + cfg = _KwOnlyVersioned.from_dict( + { + "config_version": _TOY_VERSION_OLD, + _LEGACY_FIELD: "kwargs_value", + } + ) + assert cfg.toy_new_field == "kwargs_value" + assert cfg.config_version == _TOY_VERSION_NEW + + +def test_constructor_does_not_run_migration(_migrate_then_alias_current): + """Typed construction must not run the legacy migration chain.""" + + class _KwOnlyVersioned(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + toy_new_field: str = "" + + @register_migration(_TOY_VERSION_OLD, _TOY_VERSION_NEW, config_type="_KwOnlyVersioned") + def _kw_migrate_v98_to_v99(config: dict) -> dict: + if _LEGACY_FIELD in config: + config[_NEW_FIELD] = config.pop(_LEGACY_FIELD) + return config + + with pytest.raises(ValidationError): + _KwOnlyVersioned( + config_version=_TOY_VERSION_OLD, + **{_LEGACY_FIELD: "kwargs_value"}, + ) + + +# ============================================================================= +# Multi-step migration chain (v50 → v51 → v52 → v53) +# +# Each step renames one field, so we can verify individual steps, full chains, +# downgrades, missing intermediates, missing fields, and mid-chain errors. +# ============================================================================= + +_V50, _V51, _V52, _V53 = 50, 51, 52, 53 +_CHAIN_CONFIG_TYPE = "ChainConfig" + + +def _register_chain_migrations(): + """Register a v50↔v53 migration chain (upgrades and downgrades).""" + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v50_to_v51(config: dict) -> dict: + if "field_a_old" in config: + config["field_a_new"] = config.pop("field_a_old") + return config + + @register_migration(_V51, _V52, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v51_to_v52(config: dict) -> dict: + if "field_b_old" in config: + config["field_b_new"] = config.pop("field_b_old") + return config + + @register_migration(_V52, _V53, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v52_to_v53(config: dict) -> dict: + if "field_c_old" in config: + config["field_c_new"] = config.pop("field_c_old") + return config + + @register_migration(_V51, _V50, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v51_to_v50(config: dict) -> dict: + if "field_a_new" in config: + config["field_a_old"] = config.pop("field_a_new") + return config + + @register_migration(_V52, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v52_to_v51(config: dict) -> dict: + if "field_b_new" in config: + config["field_b_old"] = config.pop("field_b_new") + return config + + @register_migration(_V53, _V52, config_type=_CHAIN_CONFIG_TYPE) + def _chain_v53_to_v52(config: dict) -> dict: + if "field_c_new" in config: + config["field_c_old"] = config.pop("field_c_new") + return config + + +@pytest.fixture +def chain_migrations(monkeypatch): + """Register v50↔v53 chain and set CURRENT_CONFIG_VERSION=53.""" + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", _V53) + _register_chain_migrations() + + +# ----------------------------------------------------------------------------- +# Multi-step upgrade chains +# ----------------------------------------------------------------------------- + + +class TestMultiStepUpgrade: + """Tests for chained upgrade migrations across multiple versions.""" + + def test_upgrade_v50_to_v53_applies_all_steps(self, chain_migrations): + """Full chain v50→v53 renames all three fields.""" + cfg = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + "untouched": 42, + } + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert result["config_version"] == _V53 + assert result["field_a_new"] == "a" + assert result["field_b_new"] == "b" + assert result["field_c_new"] == "c" + assert result["untouched"] == 42 + assert "field_a_old" not in result + assert "field_b_old" not in result + assert "field_c_old" not in result + + def test_upgrade_v50_to_v52_partial_chain(self, chain_migrations): + """Partial chain v50→v52 renames only fields a and b.""" + cfg = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + } + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V52) + assert result["config_version"] == _V52 + assert result["field_a_new"] == "a" + assert result["field_b_new"] == "b" + assert result["field_c_old"] == "c" # untouched — v52→v53 not applied + + def test_upgrade_single_step_v51_to_v52(self, chain_migrations): + """Single step in the middle of the chain works.""" + cfg = {"config_version": _V51, "field_b_old": "b"} + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V52) + assert result["config_version"] == _V52 + assert result["field_b_new"] == "b" + + def test_upgrade_does_not_mutate_original(self, chain_migrations): + """Original config dict is untouched after multi-step upgrade.""" + cfg = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + } + original_copy = cfg.copy() + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert cfg == original_copy + + +# ----------------------------------------------------------------------------- +# Multi-step downgrade chains +# ----------------------------------------------------------------------------- + + +class TestMultiStepDowngrade: + """Tests for chained downgrade migrations across multiple versions.""" + + def test_downgrade_v53_to_v50_applies_all_steps(self, chain_migrations): + """Full downgrade v53→v50 reverses all three field renames.""" + cfg = { + "config_version": _V53, + "field_a_new": "a", + "field_b_new": "b", + "field_c_new": "c", + "untouched": 42, + } + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert result["config_version"] == _V50 + assert result["field_a_old"] == "a" + assert result["field_b_old"] == "b" + assert result["field_c_old"] == "c" + assert result["untouched"] == 42 + + def test_downgrade_v53_to_v51_partial(self, chain_migrations): + """Partial downgrade v53→v51 reverses only fields c and b.""" + cfg = { + "config_version": _V53, + "field_a_new": "a", + "field_b_new": "b", + "field_c_new": "c", + } + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert result["config_version"] == _V51 + assert result["field_a_new"] == "a" # untouched — v51→v50 not applied + assert result["field_b_old"] == "b" + assert result["field_c_old"] == "c" + + def test_downgrade_does_not_mutate_original(self, chain_migrations): + """Original config dict is untouched after multi-step downgrade.""" + cfg = { + "config_version": _V53, + "field_a_new": "a", + "field_b_new": "b", + "field_c_new": "c", + } + original_copy = cfg.copy() + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert cfg == original_copy + + +# ----------------------------------------------------------------------------- +# Multi-step round-trips +# ----------------------------------------------------------------------------- + + +class TestMultiStepRoundTrip: + """Verify that upgrade→downgrade and downgrade→upgrade round-trips preserve data.""" + + def test_roundtrip_v50_to_v53_and_back(self, chain_migrations): + original = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + "extra": [1, 2, 3], + } + upgraded = migrate_config(original, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + back = migrate_config(upgraded, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert back["config_version"] == _V50 + assert back["field_a_old"] == "a" + assert back["field_b_old"] == "b" + assert back["field_c_old"] == "c" + assert back["extra"] == [1, 2, 3] + + def test_roundtrip_v53_to_v50_and_back(self, chain_migrations): + original = { + "config_version": _V53, + "field_a_new": "a", + "field_b_new": "b", + "field_c_new": "c", + } + downgraded = migrate_config(original, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + back = migrate_config(downgraded, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert back["config_version"] == _V53 + assert back["field_a_new"] == "a" + assert back["field_b_new"] == "b" + assert back["field_c_new"] == "c" + + +# ----------------------------------------------------------------------------- +# Missing intermediate migrations +# ----------------------------------------------------------------------------- + + +class TestMissingIntermediateMigration: + """Verify that gaps in the migration chain produce clear errors.""" + + def test_missing_middle_step_raises_with_context(self, chain_migrations): + """v50→v53 with v51→v52 removed raises naming the missing step.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V51, _V52)] + cfg = {"config_version": _V50} + with pytest.raises(ValueError, match=r"No migration registered for 'ChainConfig' v51 -> v52"): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + + def test_missing_first_step_raises(self, chain_migrations): + """v50→v53 with v50→v51 removed raises naming the first step.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + cfg = {"config_version": _V50} + with pytest.raises(ValueError, match=r"No migration registered for 'ChainConfig' v50 -> v51"): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + + def test_missing_downgrade_step_raises(self, chain_migrations): + """v53→v50 with v52→v51 removed raises naming the missing step.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V52, _V51)] + cfg = {"config_version": _V53} + with pytest.raises(ValueError, match=r"No migration registered for 'ChainConfig' v52 -> v51"): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + + +# ----------------------------------------------------------------------------- +# Migration with missing / extra fields +# ----------------------------------------------------------------------------- + + +class TestMissingFields: + """Migration functions should tolerate configs that lack optional fields.""" + + def test_upgrade_with_no_renameable_fields(self, chain_migrations): + """Config without any of the fields the migrations rename still upgrades.""" + cfg = {"config_version": _V50, "unrelated": "data"} + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert result["config_version"] == _V53 + assert result["unrelated"] == "data" + assert "field_a_new" not in result + assert "field_b_new" not in result + assert "field_c_new" not in result + + def test_upgrade_with_partial_fields(self, chain_migrations): + """Only the fields present are renamed; absent ones are not invented.""" + cfg = {"config_version": _V50, "field_a_old": "a"} + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert result["field_a_new"] == "a" + assert "field_b_new" not in result + assert "field_c_new" not in result + + def test_downgrade_with_missing_fields(self, chain_migrations): + """Downgrade with missing fields: absent fields are simply absent.""" + cfg = {"config_version": _V53, "field_c_new": "c"} + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert result["config_version"] == _V50 + assert result["field_c_old"] == "c" + assert "field_a_old" not in result + assert "field_b_old" not in result + + def test_extra_unknown_fields_are_preserved(self, chain_migrations): + """Fields not touched by any migration survive the full chain.""" + cfg = { + "config_version": _V50, + "field_a_old": "a", + "totally_unknown": {"nested": True}, + } + result = migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert result["totally_unknown"] == {"nested": True} + + +# ----------------------------------------------------------------------------- +# Error handling mid-migration +# ----------------------------------------------------------------------------- + + +class TestMidMigrationError: + """If a migration function raises, the error should identify the failing step.""" + + def test_error_in_second_step_reports_step(self, chain_migrations): + """An exception in v51→v52 wraps with the step identifier.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V51, _V52)] + + @register_migration(_V51, _V52, config_type=_CHAIN_CONFIG_TYPE) + def _broken_v51_to_v52(config: dict) -> dict: + raise RuntimeError("something broke in v51→v52") + + cfg = {"config_version": _V50} + with pytest.raises( + RuntimeError, + match=r"Migration for 'ChainConfig' v51 -> v52 failed.*something broke", + ): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + + def test_error_preserves_original_config(self, chain_migrations): + """Original config is not mutated even when a mid-chain step fails.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V52, _V53)] + + @register_migration(_V52, _V53, config_type=_CHAIN_CONFIG_TYPE) + def _broken_v52_to_v53(config: dict) -> dict: + raise ValueError("boom") + + cfg = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + } + original_copy = cfg.copy() + with pytest.raises(ValueError, match="boom"): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert cfg == original_copy + + def test_error_preserves_exception_type(self, chain_migrations): + """The re-raised exception keeps its original type (not generic RuntimeError).""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V51, _V52)] + + @register_migration(_V51, _V52, config_type=_CHAIN_CONFIG_TYPE) + def _type_error_step(config: dict) -> dict: + raise TypeError("wrong type for field X") + + cfg = {"config_version": _V50} + with pytest.raises(TypeError, match="Migration for 'ChainConfig' v51 -> v52 failed"): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + + +# ----------------------------------------------------------------------------- +# register_migration validation +# ----------------------------------------------------------------------------- + + +class TestRegisterMigrationValidation: + """Edge-case validation for the registration decorator.""" + + def test_negative_from_version_raises(self): + with pytest.raises(ValueError, match="non-negative"): + + @register_migration(-1, 0, config_type=_CHAIN_CONFIG_TYPE) + def _bad(config): + return config + + def test_negative_to_version_raises(self): + with pytest.raises(ValueError, match="non-negative"): + + @register_migration(0, -1, config_type=_CHAIN_CONFIG_TYPE) + def _bad(config): + return config + + def test_same_version_raises(self): + with pytest.raises(ValueError, match="must differ"): + + @register_migration(5, 5, config_type=_CHAIN_CONFIG_TYPE) + def _bad(config): + return config + + def test_duplicate_registration_raises(self): + """Registering the same (config_type, from, to) triple twice raises.""" + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _first(config): + return config + + with pytest.raises(ValueError, match="Duplicate migration"): + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _second(config): + return config + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- + + +class TestMigrationLogging: + """Verify that migration logging reports field changes.""" + + def test_upgrade_logs_info_start_and_complete(self, caplog, chain_migrations): + """INFO logs report migration start (with config type, direction) and completion.""" + cfg = {"config_version": _V50, "field_a_old": "a"} + with caplog.at_level(logging.INFO, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert "Migrating ChainConfig from version 50 to 53 (upgrade)" in caplog.text + assert "Migration complete: ChainConfig is now at version 53" in caplog.text + + def test_downgrade_logs_info_direction(self, caplog, chain_migrations): + """INFO logs report 'downgrade' direction.""" + cfg = {"config_version": _V53, "field_c_new": "c"} + with caplog.at_level(logging.INFO, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert "Migrating ChainConfig from version 53 to 50 (downgrade)" in caplog.text + assert "Migration complete: ChainConfig is now at version 50" in caplog.text + + def test_same_version_no_log(self, caplog, chain_migrations): + """When source == target, nothing is logged.""" + cfg = {"config_version": _V50} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + assert caplog.text == "" + + def test_debug_logs_field_rename(self, caplog, chain_migrations): + """DEBUG logs report removed and added fields for a rename.""" + cfg = {"config_version": _V50, "field_a_old": "a"} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Removed field 'field_a_old'" in caplog.text + assert "Added field 'field_a_new'" in caplog.text + + def test_debug_logs_updated_field(self, caplog, chain_migrations): + """DEBUG logs report a field whose value changed in-place.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _mutate_field(config: dict) -> dict: + config["keep_me"] = config.get("keep_me", 0) + 100 + return config + + cfg = {"config_version": _V50, "keep_me": 1} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Updated field 'keep_me': 1 -> 101" in caplog.text + + def test_debug_logs_no_field_changes(self, caplog, chain_migrations): + """When migration changes nothing, DEBUG reports 'No field changes'.""" + cfg = {"config_version": _V50} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "No field changes" in caplog.text + + def test_debug_logs_applying_migration_function_name(self, caplog, chain_migrations): + """DEBUG logs include the config type and qualified name of each migration function.""" + cfg = {"config_version": _V50, "field_a_old": "a"} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Applying migration ChainConfig v50 -> v51" in caplog.text + + def test_multi_step_upgrade_logs_all_steps(self, caplog, chain_migrations): + """A multi-step upgrade logs each intermediate step.""" + cfg = { + "config_version": _V50, + "field_a_old": "a", + "field_b_old": "b", + "field_c_old": "c", + } + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V53) + assert "Applying migration ChainConfig v50 -> v51" in caplog.text + assert "Applying migration ChainConfig v51 -> v52" in caplog.text + assert "Applying migration ChainConfig v52 -> v53" in caplog.text + + def test_info_not_shown_when_no_migration(self, caplog, chain_migrations): + """When no migration is needed, no INFO messages are emitted.""" + cfg = {"config_version": _V50} + with caplog.at_level(logging.INFO, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V50) + info_records = [r for r in caplog.records if r.levelno >= logging.INFO] + assert len(info_records) == 0 + + def test_debug_logs_nested_dict_added_key(self, caplog, chain_migrations): + """DEBUG logs report a key added inside a nested dict.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _add_nested(config: dict) -> dict: + config.setdefault("video_sets", {}) + config["video_sets"]["new_key"] = "new_value" + return config + + cfg = {"config_version": _V50, "video_sets": {}} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Added field 'video_sets.new_key' = 'new_value'" in caplog.text + + def test_debug_logs_nested_dict_removed_key(self, caplog, chain_migrations): + """DEBUG logs report a key removed inside a nested dict.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _remove_nested(config: dict) -> dict: + config["opts"].pop("old_opt") + return config + + cfg = {"config_version": _V50, "opts": {"old_opt": 1, "keep": 2}} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Removed field 'opts.old_opt' (was: 1)" in caplog.text + + def test_debug_logs_nested_dict_updated_value(self, caplog, chain_migrations): + """DEBUG logs report a value changed inside a nested dict.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _update_nested(config: dict) -> dict: + config["settings"]["threshold"] = 0.9 + return config + + cfg = {"config_version": _V50, "settings": {"threshold": 0.5}} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Updated field 'settings.threshold': 0.5 -> 0.9" in caplog.text + + def test_debug_logs_deeply_nested_change(self, caplog, chain_migrations): + """DEBUG logs report changes multiple levels deep.""" + del versioning._MIGRATIONS[(_CHAIN_CONFIG_TYPE, _V50, _V51)] + + @register_migration(_V50, _V51, config_type=_CHAIN_CONFIG_TYPE) + def _deep_change(config: dict) -> dict: + config["a"]["b"]["c"] = "new" + return config + + cfg = {"config_version": _V50, "a": {"b": {"c": "old"}}} + with caplog.at_level(logging.DEBUG, logger=_LOGGER_NAME): + migrate_config(cfg, config_type=_CHAIN_CONFIG_TYPE, target_version=_V51) + assert "Updated field 'a.b.c': 'old' -> 'new'" in caplog.text + + +# ----------------------------------------------------------------------------- +# DLCVersionedConfig: from_yaml / to_yaml integration with migration +# ----------------------------------------------------------------------------- + + +class _FileVersionedConfig(DLCVersionedConfig): + config_version: int = _TOY_VERSION_NEW + toy_new_field: str = "" + other: str = "default" + + +class TestFromYamlMigration: + """from_yaml and to_yaml must interact correctly with the migration system.""" + + @pytest.fixture(autouse=True) + def _file_versioned_migrations(self, register_toy_migrations): + register_toy_migrations("_FileVersionedConfig") + + def test_from_yaml_with_old_version_runs_migration(self, tmp_path): + """A YAML file at an old config_version is migrated on load.""" + path = tmp_path / "config.yaml" + path.write_text(f"config_version: {_TOY_VERSION_OLD}\n{_LEGACY_FIELD}: migrated_value\nother: kept\n") + cfg = _FileVersionedConfig.from_yaml(path) + assert cfg.toy_new_field == "migrated_value" + assert cfg.other == "kept" + assert cfg.config_version == _TOY_VERSION_NEW + + def test_from_yaml_current_version_no_migration(self, tmp_path): + """A YAML file already at the current version loads without migration.""" + path = tmp_path / "config.yaml" + path.write_text(f"config_version: {_TOY_VERSION_NEW}\ntoy_new_field: already_current\n") + cfg = _FileVersionedConfig.from_yaml(path) + assert cfg.toy_new_field == "already_current" + + def test_to_yaml_persists_new_config_version(self, tmp_path): + """After migration, to_yaml writes the new config_version to disk.""" + path = tmp_path / "config.yaml" + path.write_text(f"config_version: {_TOY_VERSION_OLD}\n{_LEGACY_FIELD}: value\n") + cfg = _FileVersionedConfig.from_yaml(path) + cfg.to_yaml(path) + + from deeplabcut.core.config.utils import read_config_as_dict + + saved = read_config_as_dict(path) + assert saved["config_version"] == _TOY_VERSION_NEW + assert "toy_new_field" in saved + assert _LEGACY_FIELD not in saved + + def test_to_yaml_then_from_yaml_roundtrip(self, tmp_path): + """Writing a migrated config and reloading it should be a perfect no-op.""" + path = tmp_path / "config.yaml" + path.write_text(f"config_version: {_TOY_VERSION_OLD}\n{_LEGACY_FIELD}: round_trip\nother: preserved\n") + cfg_first = _FileVersionedConfig.from_yaml(path) + cfg_first.to_yaml(path) + cfg_second = _FileVersionedConfig.from_yaml(path) + assert cfg_second.toy_new_field == "round_trip" + assert cfg_second.other == "preserved" + assert cfg_second.config_version == _TOY_VERSION_NEW + assert not cfg_second.is_dirty + + +# ----------------------------------------------------------------------------- +# DLCVersionedConfig compatibility with validate_assignment +# ----------------------------------------------------------------------------- + + +class ValidatedMigratingConfig(DLCVersionedConfig): + name: str = "default" + count: int = 0 + + +class TestVersionedConfigValidateAssignment: + """Versioned config must not interfere with validate_assignment (regression).""" + + @pytest.fixture(autouse=True) + def _default_version(self, monkeypatch): + monkeypatch.setattr(versioning, "CURRENT_CONFIG_VERSION", 0) + + def test_valid_assignment_takes_effect(self): + cfg = ValidatedMigratingConfig() + cfg.count = 42 + assert cfg.count == 42 + + def test_invalid_assignment_raises(self): + cfg = ValidatedMigratingConfig() + with pytest.raises(ValidationError): + cfg.count = "not-an-int" + assert cfg.count == 0 + + def test_coercion_applied_on_assignment(self): + cfg = ValidatedMigratingConfig() + cfg.count = True # coerced to 1 + assert cfg.count == 1 diff --git a/tests/core/debug/test_debug_logger.py b/tests/core/debug/test_debug_logger.py new file mode 100644 index 0000000000..c00a4566b6 --- /dev/null +++ b/tests/core/debug/test_debug_logger.py @@ -0,0 +1,483 @@ +from __future__ import annotations + +import logging +from uuid import uuid4 + +import pytest + +import deeplabcut.core.debug.debug_logger as debug_mod +from deeplabcut.core.debug import ( + DebugSection, + ExecutableSpec, + InMemoryDebugRecorder, + LibrarySpec, + build_debug_report, + collect_executable_summary, + collect_version_summary, + format_debug_report, + get_debug_recorder, + install_debug_recorder, + log_timing, +) + + +@pytest.fixture +def logger_name() -> str: + return f"deeplabcut.tests.debug.{uuid4()}" + + +@pytest.fixture +def clean_logger(logger_name: str): + """Create an isolated logger namespace and fully clean it afterwards.""" + logger = logging.getLogger(logger_name) + old_level = logger.level + old_propagate = logger.propagate + old_handlers = list(logger.handlers) + + logger.setLevel(logging.DEBUG) + logger.propagate = False + + yield logger + + for handler in list(logger.handlers): + logger.removeHandler(handler) + try: + handler.close() + except Exception: + pass + + for handler in old_handlers: + logger.addHandler(handler) + + logger.setLevel(old_level) + logger.propagate = old_propagate + + # Remove recorder marker installed by install_debug_recorder(). + logger.__dict__.pop("_dlc_debug_recorder", None) + + +def test_install_debug_recorder_is_idempotent(logger_name: str, clean_logger): + recorder1 = install_debug_recorder(logger_name=logger_name, capacity=10) + recorder2 = install_debug_recorder(logger_name=logger_name, capacity=99) + + assert recorder1 is recorder2 + assert isinstance(recorder1, InMemoryDebugRecorder) + assert get_debug_recorder(logger_name=logger_name) is recorder1 + + +def test_recorder_captures_messages_and_exceptions(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=10, handler_level=logging.DEBUG) + + logger.info("hello %s", "dlc") + try: + raise ValueError("boom") + except ValueError: + logger.exception("something failed") + + records = recorder.snapshot() + + assert len(records) == 2 + assert records[0].message == "hello dlc" + assert records[0].level == "INFO" + assert records[1].message == "something failed" + assert records[1].level == "ERROR" + assert records[1].exc_text is not None + assert "ValueError: boom" in records[1].exc_text + + +def test_recorder_is_bounded(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=2, handler_level=logging.DEBUG) + + logger.debug("first") + logger.debug("second") + logger.debug("third") + + messages = [rec.message for rec in recorder.snapshot()] + assert messages == ["second", "third"] + + +def test_render_text_contains_recent_messages(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=5) + + logger.warning("alpha") + logger.error("beta") + + text = recorder.render_text(limit=10) + + assert "WARNING" in text + assert "ERROR" in text + assert "alpha" in text + assert "beta" in text + assert logger_name in text + + +def test_clear_resets_records_and_drop_count(logger_name: str, clean_logger): + logger = clean_logger + recorder = install_debug_recorder(logger_name=logger_name, capacity=5) + + logger.info("before clear") + assert recorder.snapshot() + + recorder.clear() + + assert recorder.snapshot() == [] + assert recorder.dropped_count == 0 + assert recorder.render_text() == "" + + +def test_log_timing_emits_when_enabled( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", True) + + ticks = iter([1_000_000_000, 1_005_000_000]) # 5.000 ms + monkeypatch.setitem(wrapped.__globals__, "perf_counter_ns", lambda: next(ticks)) + + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "tiny-step", threshold_ms=0.0): + pass + + assert calls == [ + (logging.DEBUG, "%s took %.3f ms", ("tiny-step", 5.0)), + ] + + +def test_log_timing_is_silent_when_disabled( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", False) + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "should-not-appear", threshold_ms=0.0): + pass + + assert calls == [] + + +def test_log_timing_respects_threshold( + monkeypatch: pytest.MonkeyPatch, + logger_name: str, + clean_logger, +): + logger = clean_logger + calls: list[tuple[int, str, tuple[object, ...]]] = [] + + wrapped = log_timing.__wrapped__ + + monkeypatch.setitem(wrapped.__globals__, "DLC_LOG_TIMING", True) + + ticks = iter([1_000_000_000, 1_001_000_000]) # 1.000 ms + monkeypatch.setitem(wrapped.__globals__, "perf_counter_ns", lambda: next(ticks)) + + monkeypatch.setattr(logger, "isEnabledFor", lambda level: True) + + def fake_log(level, msg, *args): + calls.append((level, msg, args)) + + monkeypatch.setattr(logger, "log", fake_log) + + with log_timing(logger, "tiny-step", threshold_ms=2.0): + pass + + assert calls == [] + + +# ----------- Report building tests ----------- +def test_build_debug_report_includes_runtime_libraries_tools_and_recent_logs( + monkeypatch: pytest.MonkeyPatch, +): + recorder = InMemoryDebugRecorder(capacity=10, level=logging.DEBUG) + + record = logging.LogRecord( + name="deeplabcut.tests.debug", + level=logging.INFO, + pathname=__file__, + lineno=123, + msg="hello %s", + args=("report",), + exc_info=None, + ) + recorder.handle(record) + + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.11.9", + "platform": "TestOS-1.0", + "executable": "bin/python", + }, + ) + + monkeypatch.setattr( + debug_mod, + "_version", + lambda dist_name: { + "alpha": "1.2.3", + "opencv-python": "9.9.9-dist", + }.get(dist_name, "not-installed"), + ) + + monkeypatch.setattr( + debug_mod, + "_module_version", + lambda module_name: { + "cv2": "4.10.0", + }.get(module_name, "not-installed"), + ) + + monkeypatch.setattr( + debug_mod, + "_module_path", + lambda module_name: { + "alpha": "/tmp/site-packages/alpha/__init__.py", + "cv2": "/tmp/site-packages/cv2/__init__.py", + }.get(module_name, "unknown"), + ) + + monkeypatch.setattr( + debug_mod, + "_command_version", + lambda command, version_args: { + "ffmpeg": "ffmpeg 6.1", + }.get(command, "unavailable"), + ) + + monkeypatch.setattr( + debug_mod, + "_which", + lambda command: { + "ffmpeg": "/usr/bin/ffmpeg", + }.get(command, "not-found"), + ) + + report = build_debug_report( + recorder=recorder, + libraries=( + LibrarySpec("alpha"), + LibrarySpec( + "opencv-python", + dist_name="opencv-python", + module_name="cv2", + prefer_module_version=True, + ), + ), + executables=(ExecutableSpec("ffmpeg"),), + include_module_paths=True, + include_executable_paths=True, + log_limit=20, + ) + + assert "## Runtime" in report + assert "- python: 3.11.9" in report + assert "- platform: TestOS-1.0" in report + assert "- executable: bin/python" in report + + assert "## Libraries" in report + assert "- alpha: 1.2.3" in report + assert "- opencv-python: 4.10.0" in report + assert "- alpha_module_path: alpha/__init__.py" in report + assert "- opencv-python_module_path: cv2/__init__.py" in report + + assert "## External tools" in report + assert "- ffmpeg: ffmpeg 6.1" in report + assert "- ffmpeg_path: bin/ffmpeg" in report + + assert "## Recent logs" in report + assert "deeplabcut.tests.debug" in report + assert "INFO" in report + assert "hello report" in report + assert "```text" in report + + +def test_build_debug_report_default_grouped_sections_and_skips_unavailable_tf( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.12.0", + "platform": "GroupedTestOS", + "executable": "python", + }, + ) + + def fake_collect_version_summary(*, libraries=None, include_module_paths=False): + if libraries == debug_mod.DLC_CORE_LIBS: + return {"deeplabcut": "1.0.0", "numpy": "2.0.0"} + if libraries == debug_mod.DLC_GUI_LIBS: + return {"PySide6": "6.8.0"} + if libraries == debug_mod.DLC_TF_LIBS: + return { + "tensorflow": "not-installed", + "tf_keras": "not-installed", + "tensorpack": "unknown", + "tf_slim": "not-installed", + } + raise AssertionError("unexpected libraries input") + + monkeypatch.setattr(debug_mod, "collect_version_summary", fake_collect_version_summary) + + monkeypatch.setattr( + debug_mod, + "collect_executable_summary", + lambda *, executables=None, include_paths=True: { + "ffmpeg": "unavailable", + "ffmpeg_path": "not-found", + }, + ) + + report = build_debug_report( + recorder=None, + libraries=None, + executables=None, + ) + + assert "## Runtime" in report + assert "## DeepLabCut core libraries" in report + assert "- deeplabcut: 1.0.0" in report + assert "## GUI libraries" in report + assert "- PySide6: 6.8.0" in report + + # All TF values are unavailable/unknown, so the section should be omitted. + assert "## TensorFlow libraries" not in report + + # External tools should still be shown even when unavailable. + assert "## External tools" in report + assert "- ffmpeg: unavailable" in report + assert "- ffmpeg_path: not-found" in report + + assert "## Recent logs" in report + assert "" in report + + +def test_collect_version_summary_prefers_module_version_and_falls_back_to_distribution( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr( + debug_mod, + "_module_version", + lambda module_name: { + "cv2": "not-installed", + }.get(module_name, "unknown"), + ) + + monkeypatch.setattr( + debug_mod, + "_version", + lambda dist_name: { + "opencv-python": "4.9.0.80", + "missing-lib": "not-installed", + }.get(dist_name, "not-installed"), + ) + + summary = collect_version_summary( + libraries=( + LibrarySpec( + "opencv-python", + dist_name="opencv-python", + module_name="cv2", + prefer_module_version=True, + ), + LibrarySpec("missing-lib"), + ) + ) + + assert summary["opencv-python"] == "4.9.0.80" + assert summary["missing-lib"] == "not-installed" + + +def test_collect_executable_summary_reports_unavailable_tool_and_path( + monkeypatch: pytest.MonkeyPatch, +): + monkeypatch.setattr(debug_mod, "_command_version", lambda command, version_args: "unavailable") + monkeypatch.setattr(debug_mod, "_which", lambda command: "not-found") + + summary = collect_executable_summary( + executables=(ExecutableSpec("ghosttool"),), + include_paths=True, + ) + + assert summary == { + "ghosttool": "unavailable", + "ghosttool_path": "not-found", + } + + +def test_build_debug_report_uses_no_captured_logs_placeholder_for_empty_recorder( + monkeypatch: pytest.MonkeyPatch, +): + recorder = InMemoryDebugRecorder(capacity=5, level=logging.DEBUG) + + monkeypatch.setattr( + debug_mod, + "collect_runtime_summary", + lambda: { + "python": "3.11.0", + "platform": "EmptyLogsOS", + "executable": "python", + }, + ) + + monkeypatch.setattr( + debug_mod, + "collect_executable_summary", + lambda *, executables=None, include_paths=True: {}, + ) + + report = build_debug_report( + recorder=recorder, + libraries=(), + executables=(), + ) + + assert "## Runtime" in report + assert "## Libraries" in report + assert "- " in report + assert "## Recent logs" in report + assert "" in report + + +def test_format_debug_report_renders_empty_section_and_logs_block(): + text = format_debug_report( + sections=[ + DebugSection(title="Example", items={}), + ], + logs_text="line one\nline two", + ) + + assert "## Example" in text + assert "- " in text + assert "## Recent logs" in text + assert "```text" in text + assert "line one" in text + assert "line two" in text diff --git a/tests/core/inferenceutils/test_map_computation.py b/tests/core/inferenceutils/test_map_computation.py new file mode 100644 index 0000000000..c2cd4fffe9 --- /dev/null +++ b/tests/core/inferenceutils/test_map_computation.py @@ -0,0 +1,418 @@ +"""Tests mAP computation from inferenceutils.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from deeplabcut.core import inferenceutils +from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + ], + }, + ], +) +@pytest.mark.parametrize( + "predictions", + [ + { + "img0": [ + [ + [100.0, 10.0, 0.9], + [150.0, 15.0, 0.7], + [202.0, 20.0, 0.8], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [140.0, 17.0, 0.7], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [np.nan, np.nan, 0.0], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + ], +) +def test_map_single_image_simple(ground_truth: dict, predictions: dict): + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {k: np.array(v) for k, v in predictions.items()} + _evaluate(gt, pred) + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [326.0, 236.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0.0, 0.0, 0], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0, 0, 0], + [457.0, 832.0, 2], + ], + [ + [452.0, 321.0, 2], + [213.0, 387.0, 2], + [213.0, 832.0, 2], + ], + [ + [253.0, 238.0, 2], + [213.0, 238.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + ], +) +def test_map_single_image_random_errors(ground_truth: dict): + rng = np.random.default_rng(seed=0) + + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {} + for k, gt_kpts in gt.items(): + num_idv, num_bpt = gt_kpts.shape[:2] + + error = rng.integers(low=-30, high=30, size=(num_idv, num_bpt, 2)) + scores = rng.random(size=(num_idv, num_bpt)) + + pred[k] = np.zeros(shape=(num_idv, num_bpt, 3)) + pred[k][..., :2] = np.clip(gt_kpts[..., :2] + error, 0, 1024) + pred[k][..., 2] = scores + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + max_idv = num_individuals.max(initial=0) + + gt = {} + pred = {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + # padding needed as we then stack + gt_kpts = np.zeros((max_idv, num_joints, 3)) + 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[f"img_{i}"] = gt_kpts + + # set scores + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # 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)) + 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[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + max_idv = num_individuals.max(initial=0) + + gt = {} + pred = {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + # padding needed as we then stack + gt_kpts = np.zeros((max_idv, num_joints, 3)) + 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[f"img_{i}"] = gt_kpts + + # drop some ground truth keypoints + gt_vis_mask = rng.random(size=(max_idv, num_joints)) < 0.2 + gt_kpts[gt_vis_mask, 2] = 0 + + # set scores + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # 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)) + 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[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): + for k, v in gt.items(): + print(20 * "-") + print(k) + print("GT") + print(v) + print("PR") + print(pred[k]) + + gt_assemblies = _to_assemblies(gt, ground_truth=True) + pred_assemblies = _to_assemblies(pred, ground_truth=False) + oks = inferenceutils.evaluate_assembly_greedy( + assemblies_gt=gt_assemblies, + assemblies_pred=pred_assemblies, + oks_sigma=0.1, + oks_thresholds=np.linspace(0.5, 0.95, 10), + margin=0.0, + symmetric_kpts=None, + ) + + num_joints = gt[list(gt.keys())[0]].shape[1] + coco_gt = _to_coco_ground_truth(gt, num_joints, bbox_margin=0) + coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) + coco_oks = eval_coco(coco_gt, coco_pred, num_joints) + print(20 * "-") + print("dlc mAP:") + for k, v in oks.items(): + print(k) + print(v) + print() + print(20 * "-") + print(f"pycocotools mAP: {coco_oks}") + print() + assert oks["mAP"] == coco_oks + + +def _to_assemblies( + 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) + + # mask not visible entries + mask = raw_data[..., 2] <= 0 + raw_data[mask] = np.nan + + # set the "score" to 1 for ground truth + if ground_truth: + raw_data[~mask, 2] = 1 + + return {images[i]: assembly for i, assembly in inferenceutils._parse_ground_truth_data(raw_data).items()} + + +def _to_coco_ground_truth( + data: dict[str, np.ndarray], + num_joints: int, + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> dict[str, list[dict]]: + w, h = image_size + anns, images = [], [] + for path, image_keypoints in data.items(): + id_ = len(images) + 1 + 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): + visible = kpts[:, 2] > 0 + num_keypoints = visible.sum() + + if num_keypoints > 1: + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + anns.append( + { + "id": len(anns) + 1, + "image_id": id_, + "category_id": 1, + "area": area, + "bbox": bbox.tolist(), + "keypoints": kpts.reshape(-1).tolist(), + "iscrowd": 0, + "num_keypoints": num_keypoints, + } + ) + + keypoints = [f"bpt{i}" for i in range(num_joints)] + category = dict(id=1, name="animal", supercategory="animal", keypoints=keypoints) + return {"annotations": anns, "categories": [category], "images": images} + + +def _to_coco_predictions( + ground_truth: dict, + predictions: dict[str, np.ndarray], + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> list[dict]: + w, h = image_size + num_joints = len(ground_truth["categories"][0]["keypoints"]) + path_to_id = {img["file_name"]: img["id"] for img in ground_truth["images"]} + + coco_predictions = [] + for path, image_keypoints in predictions.items(): + 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))] + for kpts in valid_predictions: + score = float(np.nanmean(kpts[:, 2]).item()) + kpts = kpts.copy() + kpts[:, 2] = 2 + + # NaN predictions to infinity + kpts[np.isnan(kpts)] = np.inf + + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + coco_predictions.append( + { + "image_id": img_id, + "category_id": 1, + "keypoints": kpts.reshape(-1).tolist(), + "bbox": bbox.tolist(), + "area": area, + "score": score, + } + ) + + return coco_predictions + + +def eval_coco( + ground_truth: dict, + predictions: list[dict], + num_joints: int, +) -> float | None: + try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + coco = COCO() + coco.dataset["annotations"] = ground_truth["annotations"] + coco.dataset["categories"] = ground_truth["categories"] + coco.dataset["images"] = ground_truth["images"] + coco.dataset["info"] = {"description": "Generated by DeepLabCut"} + coco.createIndex() + + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="keypoints") + coco_eval.params.kpt_oks_sigmas = np.array(num_joints * [0.1]) + coco_eval.evaluate() + coco_eval.accumulate() + coco_eval.summarize() + return float(coco_eval.stats[0]) + + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/core/metrics/test_metrics_api.py b/tests/core/metrics/test_metrics_api.py new file mode 100644 index 0000000000..a38516ab58 --- /dev/null +++ b/tests/core/metrics/test_metrics_api.py @@ -0,0 +1,110 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""General tests for the metrics API.""" + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +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]: + gt = np.arange(num_idv * num_bpt * 3).astype(float).reshape((num_idv, num_bpt, 3)) + gt[..., 2] = 2 + predictions = gt.copy() + predictions[..., 2] = 0.9 + predictions[..., :2] += error + return gt, predictions + + +def test_computing_metrics_with_no_predictions(): + gt = np.arange(5 * 6 * 3).astype(float).reshape((5, 6, 3)) + gt[..., 2] = 2 + metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": np.zeros((0, 12, 3))}, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_computing_metrics_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_metrics_with_unique_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + gt_unique, pred_unique = _get_gt_and_pred_with_constant_err(1, 8, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt={"image": gt_unique}, + unique_bodypart_poses={"image": pred_unique}, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_metrics_per_bpt_with_unique_with_constant_error(error): + # only works for small errors: otherwise another matching can be found + gt, predictions = _get_gt_and_pred_with_constant_err(5, 6, error) + gt_unique, pred_unique = _get_gt_and_pred_with_constant_err(1, 8, error) + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + unique_bodypart_gt={"image": gt_unique}, + unique_bodypart_poses={"image": pred_unique}, + per_keypoint_rmse=True, + ) + assert_almost_equal(results["rmse"], np.sqrt(2) * error) + assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) + + for bpt_idx in range(gt.shape[1]): + key = f"rmse_keypoint_{bpt_idx}" + assert key in results + assert_almost_equal(results[key], np.sqrt(2) * error) + for bpt_idx in range(gt_unique.shape[1]): + key = f"rmse_unique_keypoint_{bpt_idx}" + assert key in results + assert_almost_equal(results[key], np.sqrt(2) * error) + + +@pytest.mark.parametrize("error", [0.5, 1, 2]) +def test_computing_metrics_single_animal(error): + # only works for small errors: otherwise another matching can be found + gt = np.arange(6 * 3).astype(float).reshape((1, 6, 3)) + gt[..., 2] = 2 + predictions = gt.copy() + predictions[..., 2] = 0.9 + predictions[..., :2] += error + results = metrics.compute_metrics( + ground_truth={"image": gt}, + predictions={"image": predictions}, + single_animal=True, + unique_bodypart_gt=None, + unique_bodypart_poses=None, + ) + 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 new file mode 100644 index 0000000000..017653d59e --- /dev/null +++ b/tests/core/metrics/test_metrics_identity_accuracy.py @@ -0,0 +1,219 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for the scoring methods.""" + +import numpy as np +import pytest + +import deeplabcut.core.metrics.identity + + +@pytest.mark.parametrize( + "data", + [ + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[2.0, 2.0, 0.8]], + [[1.0, 1.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.8, 0.5]], + [[0.51, 0.49]], + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[1.0, 1.0, 2]], + [[0, 0, 0]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.4, 0.6]], + [[0.6, 0.4]], + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], + [[1.0, 1.0, 2]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.6, 0.4]], + [[0.6, 0.4]], # both assemblies assigned to idv 1 + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], + [[1.0, 1.0, 2]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 0.5, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7]], + [[2.0, 2.0, 0.7]], # x, y, score + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.6, 0.4]], + [[0.4, 0.6]], # both assigned to wrong ID + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2]], # x, y, visibility + [[1.0, 1.0, 2]], + ] + }, + "accuracy": { + "arm_accuracy": 0.0, + }, + }, + { + "individuals": ["i1", "i2"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3], [0.6, 0.2]], + [[0.6, 0.3], [0.6, 0.2]], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2", "i3"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + [[110.0, 110.0, 0.9], [98.0, 91.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3, 0.0], [0.6, 0.2, 0.2]], # assigned to correct ID + [[0.6, 0.3, 0.1], [0.6, 0.2, 0.2]], # should not matter, not assigned to GT + [[0.6, 0.3, 0.1], [0.6, 0.2, 0.2]], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 1.0, + }, + }, + { + "individuals": ["i1", "i2", "i3"], + "bodyparts": ["arm", "leg"], + "predictions": { + "img0.png": [ # (num_assemblies, num_bodyparts, 3) + [[1.0, 1.0, 0.7], [10.0, 10.0, 0.9]], + [[100.0, 100.0, 0.9], [90.0, 90.9, 0.8]], + [[110.0, 110.0, 0.9], [98.0, 91.9, 0.8]], + ], + }, + "identity_scores": { + "img0.png": [ # (num_assemblies, num_bodyparts, num_individuals) + [[0.7, 0.3, 0.1], [0.6, 0.2, 0.1]], # assigned to correct ID + [[0.1, 0.2, 0.7], [0.4, 0.3, 0.2]], # 1st correct, 2nd wrong + [ + [0.6, 0.3, 0.5], + [0.6, 0.2, 0.4], + ], # should not matter, not assigned to GT + ], + }, + "ground_truth": { + "img0.png": [ # (num_individuals, num_bodyparts, 3) + [[2.0, 2.0, 2], [8.0, 8.0, 2]], # x, y, visibility + [[-1, -1, 0.0], [-1, -1, 0.0]], # not visible + [[90.0, 90, 2], [80, 80, 2.0]], # x, y, visibility + ] + }, + "accuracy": { + "arm_accuracy": 1.0, + "leg_accuracy": 0.5, + }, + }, + ], +) +def test_id_accuracy(data) -> None: + scores = deeplabcut.core.metrics.identity.compute_identity_scores( + individuals=data["individuals"], + bodyparts=data["bodyparts"], + predictions={k: np.array(v) for k, v in data["predictions"].items()}, + identity_scores={k: np.array(v) for k, v in data["identity_scores"].items()}, + ground_truth={k: np.array(v) for k, v in data["ground_truth"].items()}, + ) + assert scores == data["accuracy"] diff --git a/tests/core/metrics/test_metrics_map_computation.py b/tests/core/metrics/test_metrics_map_computation.py new file mode 100644 index 0000000000..5ef6a64f75 --- /dev/null +++ b/tests/core/metrics/test_metrics_map_computation.py @@ -0,0 +1,399 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests that mAP computation is correct.""" + +from __future__ import annotations + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +from deeplabcut.core.metrics.api import prepare_evaluation_data +from deeplabcut.core.metrics.distance_metrics import compute_oks +from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + ], + }, + ], +) +@pytest.mark.parametrize( + "predictions", + [ + { + "img0": [ + [ + [100.0, 10.0, 0.9], + [150.0, 15.0, 0.7], + [202.0, 20.0, 0.8], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [140.0, 17.0, 0.7], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 0.9], + [np.nan, np.nan, 0.0], + [192.0, 22.0, 0.8], + ], + [ + [97.0, 11.0, 0.5], + [148.0, 14.0, 0.2], + [202.0, 21.0, 0.3], + ], + ], + }, + ], +) +def test_map_single_image_simple(ground_truth: dict, predictions: dict): + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {k: np.array(v) for k, v in predictions.items()} + _evaluate(gt, pred) + + +@pytest.mark.parametrize( + "ground_truth", + [ + { + "img0": [ + [ + [100.0, 10.0, 2], + [150.0, 15.0, 2], + [202.0, 20.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [326.0, 236.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0.0, 0.0, 0], + [457.0, 832.0, 2], + ], + ], + }, + { + "img0": [ + [ + [90.0, 12.0, 2], + [140.0, 17.0, 2], + [192.0, 22.0, 2], + ], + [ + [726.0, 325.0, 2], + [0, 0, 0], + [457.0, 832.0, 2], + ], + [ + [452.0, 321.0, 2], + [213.0, 387.0, 2], + [213.0, 832.0, 2], + ], + [ + [253.0, 238.0, 2], + [213.0, 238.0, 2], + [457.0, 832.0, 2], + ], + ], + }, + ], +) +def test_map_single_image_random_errors(ground_truth: dict): + rng = np.random.default_rng(seed=0) + + gt = {k: np.array(v) for k, v in ground_truth.items()} + pred = {} + for k, gt_kpts in gt.items(): + num_idv, num_bpt = gt_kpts.shape[:2] + + error = rng.integers(low=-30, high=30, size=(num_idv, num_bpt, 2)) + scores = rng.random(size=(num_idv, num_bpt)) + + pred[k] = np.zeros(shape=(num_idv, num_bpt, 3)) + pred[k][..., :2] = np.clip(gt_kpts[..., :2] + error, 0, 1024) + pred[k][..., 2] = scores + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + + gt, pred = {}, {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + gt_kpts = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[..., :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # create predictions array + pred_kpts = np.zeros((pred_idv, num_joints, 3)) + # set scores + pred_kpts[..., 2] = rng.random(size=(pred_idv, num_joints)) + + # 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)) + 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[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +@pytest.mark.parametrize("num_images", [1, 2, 5, 10]) +@pytest.mark.parametrize("num_joints", [2, 5, 8, 20]) +@pytest.mark.parametrize("max_error", [1, 2, 5, 20, 40]) +def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_error): + rng = np.random.default_rng(seed=0) + num_individuals = rng.integers(low=0, high=20, size=(num_images, 2)) + + gt, pred = {}, {} + for i, (gt_idv, pred_idv) in enumerate(num_individuals): + gt_kpts = 2 * np.ones((gt_idv, num_joints, 3)) + gt_kpts[..., :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) + gt[f"img_{i}"] = gt_kpts + + # drop some ground truth keypoints + gt_vis_mask = rng.random(size=(gt_idv, num_joints)) < 0.2 + gt_kpts[gt_vis_mask, 2] = 0 + + # generate predicted keypoints + pred_kpts = np.zeros((pred_idv, num_joints, 3)) + pred_kpts[:pred_idv, :, 2] = rng.random(size=(pred_idv, num_joints)) + + # 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)) + 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[f"img_{i}"] = pred_kpts + + _evaluate(gt, pred) + + +def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): + for k, v in gt.items(): + print(20 * "-") + print(k) + print("GT") + print(v) + print("PR") + print(pred[k]) + + data = prepare_evaluation_data(gt, pred) + oks = compute_oks(data, oks_bbox_margin=0) + + num_joints = gt[list(gt.keys())[0]].shape[1] + coco_gt = _to_coco_ground_truth(gt, num_joints, bbox_margin=0) + coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) + coco_oks = eval_coco(coco_gt, coco_pred, num_joints) + print(20 * "-") + print("dlc mAP:") + for k, v in oks.items(): + print(k) + print(v) + print(20 * "-") + print(f"pycocotools mAP: {coco_oks}") + print() + dlc_map = oks["mAP"] / 100 + assert_almost_equal(dlc_map, coco_oks) + + +def _to_coco_ground_truth( + data: dict[str, np.ndarray], + num_joints: int, + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> dict[str, list[dict]]: + w, h = image_size + anns, images = [], [] + for path, image_keypoints in data.items(): + id_ = len(images) + 1 + 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): + visible = kpts[:, 2] > 0 + num_keypoints = visible.sum() + + if num_keypoints > 1: + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + anns.append( + { + "id": len(anns) + 1, + "image_id": id_, + "category_id": 1, + "area": area, + "bbox": bbox.tolist(), + "keypoints": kpts.reshape(-1).tolist(), + "iscrowd": 0, + "num_keypoints": num_keypoints, + } + ) + + keypoints = [f"bpt{i}" for i in range(num_joints)] + category = dict(id=1, name="animal", supercategory="animal", keypoints=keypoints) + return { + "info": {"description": "Generated COCO ground truth dataset"}, # Add this + "annotations": anns, + "categories": [category], + "images": images, + } + + +def _to_coco_predictions( + ground_truth: dict, + predictions: dict[str, np.ndarray], + bbox_margin: int = 0, + image_size: tuple[int, int] = (1024, 1024), +) -> list[dict]: + w, h = image_size + num_joints = len(ground_truth["categories"][0]["keypoints"]) + path_to_id = {img["file_name"]: img["id"] for img in ground_truth["images"]} + + coco_predictions = [] + for path, image_keypoints in predictions.items(): + 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))] + for kpts in valid_predictions: + score = float(np.nanmean(kpts[:, 2]).item()) + kpts = kpts.copy() + kpts[:, 2] = 2 + + # NaN predictions to infinity + kpts[np.isnan(kpts)] = np.inf + + bbox = bbox_from_keypoints( + keypoints=kpts, + image_h=h, + image_w=w, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + coco_predictions.append( + { + "image_id": img_id, + "category_id": 1, + "keypoints": kpts.reshape(-1).tolist(), + "bbox": bbox.tolist(), + "area": area, + "score": score, + } + ) + + return coco_predictions + + +def eval_coco( + ground_truth: dict, + predictions: list[dict], + num_joints: int, +) -> float | None: + try: + from pycocotools.coco import COCO + from pycocotools.cocoeval import COCOeval + + coco = COCO() + coco.dataset["annotations"] = ground_truth["annotations"] + coco.dataset["categories"] = ground_truth["categories"] + coco.dataset["images"] = ground_truth["images"] + coco.dataset["info"] = {"description": "Generated by DeepLabCut"} + coco.createIndex() + + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="keypoints") + coco_eval.params.kpt_oks_sigmas = np.array(num_joints * [0.1]) + coco_eval.evaluate() + coco_eval.accumulate() + coco_eval.summarize() + return float(coco_eval.stats[0]) + + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/core/metrics/test_metrics_rmse_computation.py b/tests/core/metrics/test_metrics_rmse_computation.py new file mode 100644 index 0000000000..187279df31 --- /dev/null +++ b/tests/core/metrics/test_metrics_rmse_computation.py @@ -0,0 +1,374 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests RMSE computation.""" + +import numpy as np +import pytest +from numpy.testing import assert_almost_equal + +from deeplabcut.core.metrics.distance_metrics import ( + compute_detection_rmse, + compute_rmse, +) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[100.0, 10.0, 2], [150.0, 15.0, 2], [200.0, 20.0, 2]], + ], + [ # predicted pose + [[100.0, 10.0, 0.9], [150.0, 15.0, 0.8], [200.0, 20.0, 0.8]], + ], + (0, 0), + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [12.0, 10.0, 0.9], [12.0, 10.0, 0.9]], + [[22.0, 20.0, 0.9], [22.0, 20.0, 0.9], [22.0, 20.0, 0.9]], + ], + (2, 2), + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 12.0, 0.9], [10.0, 12.0, 0.9], [10.0, 12.0, 0.9]], + [[20.0, 22.0, 0.9], [20.0, 22.0, 0.9], [20.0, 22.0, 0.9]], + ], + (2, 2), + ), + ], +) +def test_rmse_single_image(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + computed_results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = computed_results["rmse"], computed_results["rmse_pcutoff"] + expected_rmse, expected_rmse_cutoff = result + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 22.0, 0.2], [20.0, 22.0, 0.2], [20.0, 22.0, 0.2]], + ], + (1, 0), # 2 pixel error on half of keypoints, 0 on the other half + ), + ], +) +def test_rmse_pcutoff(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + expected_rmse, expected_rmse_cutoff = result + + computed_results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = computed_results["rmse"], computed_results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [float("nan"), float("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]], + ], + (1, 1), # only 2 valid ground truth bodyparts + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [float("nan"), float("nan"), 0]], + [[float("nan"), float("nan"), 0], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose, swapped prediction order + [[20.0, 20.0, 0.9], [21.0, 20.0, 0.9], [21.0, 20.0, 0.9]], + [[15.0, 10.0, 0.4], [15.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + (3, 1), # only 2 valid GT bodyparts + ), + ], +) +def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): + data = [(np.asarray(gt), np.asarray(pred))] + expected_rmse, expected_rmse_cutoff = result + + results = compute_rmse(data, False, pcutoff=0.6, oks_bbox_margin=10.0) + rmse, rmse_cutoff = results["rmse"], results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, data_unique, result", + [ + ( + [ # 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]], + ], + None, # unique data + (1, 1), # 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]], + ], + None, # unique data + (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, small error in pose and misassembled + [[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 + (2, 2), # pixel error of 2 on x-axis for all predictions + ), + ( + [ # 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, small error in low-conf pose and misassembled + [[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 + (1, 0), # error of 2 on half, 0 on the other half (with good conf) + ), + ( # more ground truth than detections + [ # 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]], + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + ], + [ # predicted pose, no error + [[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 + (0, 0), + ), + ( # more detections than GT + [ # ground truth pose + [[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]], + ], + [ # predicted pose, no error + [[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]], + [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], + ], + 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], [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]): + 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 + rmse, rmse_cutoff = compute_detection_rmse(data, pcutoff=0.6, data_unique=data_unique) + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, unique_gt, unique_pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.2], [20.0, 24.0, 0.2], [20.0, 20.0, 0.2]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[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), + ), + ( + [np.zeros((0, 3, 2))], # no GT pose + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[15.0, 10.0, 0.5], [11.0, 10.0, 0.9]], + ], + # 5 pixel error on 1 keypoint, 1 pixel error on the other + (3.0, 1.0), + ), + ], +) +def test_rmse_with_unique( + 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, + ) + rmse, rmse_cutoff = results["rmse"], results["rmse_pcutoff"] + assert_almost_equal(rmse, expected_rmse) + assert_almost_equal(rmse_cutoff, expected_rmse_cutoff) + + +@pytest.mark.parametrize( + "gt, pred, unique_gt, unique_pred, result", + [ + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.2], [20.0, 24.0, 0.2], [20.0, 20.0, 0.2]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[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]], + ), + ( + [ # ground truth pose + [[10.0, 10.0, 2], [10.0, 10.0, 2], [10.0, 10.0, 2]], + [[20.0, 20.0, 2], [20.0, 20.0, 2], [20.0, 20.0, 2]], + ], + [ # predicted pose + [[10.0, 12.0, 0.9], [10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], + [[20.0, 24.0, 0.7], [20.0, 24.0, 0.6], [20.0, 20.0, 0.8]], + ], + [ # Unique GT + [[10.0, 10.0, 2], [10.0, 10.0, 2]], + ], + [ # Unique Pred + [[12.0, 10.0, 0.9], [11.0, 10.0, 0.9]], + ], + [ # 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], + ], + ), + ], +) +def test_rmse_per_bodypart_with_unique( + gt: list, + pred: list, + unique_gt: list, + unique_pred: list, + 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))] + expected_rmse, expected_rmse_cutoff = result[0] + bodypart_rmse = result[1] + unique_rmse = result[2] + + results = compute_rmse( + data, + single_animal=False, + pcutoff=0.6, + data_unique=data_unique, + per_keypoint_results=True, + oks_bbox_margin=10.0, + ) + assert_almost_equal(results["rmse"], expected_rmse) + assert_almost_equal(results["rmse_pcutoff"], expected_rmse_cutoff) + for bpt_index, bpt_rmse in enumerate(bodypart_rmse): + key = f"rmse_keypoint_{bpt_index}" + assert key in results + assert_almost_equal(results[key], bpt_rmse) + + for bpt_index, bpt_rmse in enumerate(unique_rmse): + key = f"rmse_unique_keypoint_{bpt_index}" + assert key in results + assert_almost_equal(results[key], bpt_rmse) diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py new file mode 100644 index 0000000000..86e50eecc9 --- /dev/null +++ b/tests/create_project/test_video_set_configuration.py @@ -0,0 +1,264 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Unit tests for deeplabcut.create_project.new module.""" + +import logging +import warnings +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +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.""" + fake_folder = tmpdir_factory.mktemp("some_video") + video_path = Path(fake_folder) / "test_video.avi" + video_path.write_bytes(b"fake video content") + return video_path + + +@pytest.fixture +def mock_video_reader() -> VideoReader: + """Create a mock VideoReader.""" + mock_reader = Mock(spec=VideoReader) + mock_reader.get_bbox.return_value = (0, 640, 277, 624) + return mock_reader + + +@pytest.fixture +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, + mock_video_reader: VideoReader, +): + """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", + experimenter="test-user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + project_path = Path(config_path).parent + assert project_path.exists() + assert (project_path / "videos").exists() + assert (project_path / "labeled-data").exists() + assert (project_path / "training-datasets").exists() + assert (project_path / "dlc-models").exists() + + +@pytest.mark.parametrize("copy_videos", [True, False]) +def test_single_video_file( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """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", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=copy_videos, + ) + + project_path = Path(config_path).parent + video_path = project_path / "videos" / "test_video.avi" + assert video_path.exists() or video_path.is_symlink() + + # Content should match + if copy_videos: + assert mock_video_file.read_bytes() == video_path.read_bytes() + + +@pytest.mark.parametrize("copy_videos", [True, False]) +def test_video_directory( + tmpdir: Path, + video_directory: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """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", + experimenter="user", + videos=[str(video_directory)], + working_directory=str(tmpdir), + video_extensions=".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]) +def test_mixed_video_files_and_directories( + tmpdir, + mock_video_file: Path, + video_directory: Path, + mock_video_reader: VideoReader, + copy_videos: bool, +): + """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", + experimenter="user", + videos=[str(mock_video_file), str(video_directory)], + working_directory=str(tmpdir), + video_extensions=".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 + assert (videos_dir / mock_video_file.name).exists() or (videos_dir / mock_video_file.name).is_symlink() + assert (videos_dir / "video1.avi").exists() or (videos_dir / "video1.avi").is_symlink() + + +def test_empty_video_directory( + tmpdir: Path, + mock_video_reader: VideoReader, +): + """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( + project="test", + experimenter="user", + videos=[str(empty_dir)], + working_directory=str(tmpdir), + video_extensions=".avi", + copy_videos=False, + ) + # Should return "nothingcreated" when no valid videos found + assert result == "nothingcreated" or len(w) > 0 + + +def test_valid_video_included_in_config( + tmpdir: Path, + mock_video_file: Path, + mock_video_reader: VideoReader, +): + """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", + experimenter="user", + videos=[str(mock_video_file)], + working_directory=str(tmpdir), + copy_videos=False, + ) + + 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_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( + tmpdir: Path, + mock_video_file: Path, +): + """Test that invalid videos are removed from the project.""" + # Mock VideoReader to raise IOError + mock_reader = Mock(side_effect=OSError("Cannot open video")) + + with patch("deeplabcut.create_project.new.VideoReader", mock_reader): + with warnings.catch_warnings(record=True): + result = new_module.create_new_project( + project="test", + experimenter="user", + videos=[str(mock_video_file)], + 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, + mock_video_reader: VideoReader, +): + """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", + experimenter="user", + videos=[str(mock_video_file)], + 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) + assert "crop" in video_info + assert isinstance(video_info["crop"], str) diff --git a/tests/data/trimouse_assemblies.pickle b/tests/data/trimouse_assemblies.pickle deleted file mode 100644 index 9398ea899e..0000000000 Binary files a/tests/data/trimouse_assemblies.pickle and /dev/null differ diff --git a/tests/data/trimouse_calib.h5 b/tests/data/trimouse_calib.h5 deleted file mode 100644 index 44108c193c..0000000000 Binary files a/tests/data/trimouse_calib.h5 and /dev/null differ diff --git a/tests/data/trimouse_eval.pickle b/tests/data/trimouse_eval.pickle deleted file mode 100644 index 4d5a346e90..0000000000 Binary files a/tests/data/trimouse_eval.pickle and /dev/null differ diff --git a/tests/data/trimouse_full.pickle b/tests/data/trimouse_full.pickle deleted file mode 100644 index c58cf236ef..0000000000 Binary files a/tests/data/trimouse_full.pickle and /dev/null differ diff --git a/tests/data/trimouse_meta.pickle b/tests/data/trimouse_meta.pickle deleted file mode 100644 index c62dc6b89f..0000000000 Binary files a/tests/data/trimouse_meta.pickle and /dev/null differ diff --git a/tests/data/trimouse_tracklets.pickle b/tests/data/trimouse_tracklets.pickle deleted file mode 100644 index 08ddc54556..0000000000 Binary files a/tests/data/trimouse_tracklets.pickle and /dev/null differ diff --git a/tests/data/vid.avi b/tests/data/vid.avi deleted file mode 100644 index 9e2a5d50e1..0000000000 Binary files a/tests/data/vid.avi and /dev/null differ diff --git a/tests/generate_training_dataset/test_trainingset_manipulation.py b/tests/generate_training_dataset/test_trainingset_manipulation.py new file mode 100644 index 0000000000..cf78ba3996 --- /dev/null +++ b/tests/generate_training_dataset/test_trainingset_manipulation.py @@ -0,0 +1,37 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" + +from __future__ import annotations + +import pytest + +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("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: + """ + More complete tests can be run with: + "train_fraction": list(range(1, 100)) + "n_train": list(range(1, 200)) + "n_test": list(range(1, 200)) + + 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) + 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) + assert train_frac == frac diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py new file mode 100644 index 0000000000..b9e54aff7d --- /dev/null +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -0,0 +1,580 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" + +from __future__ import annotations + +import logging +import pickle +from unittest.mock import MagicMock, patch + +import pytest + +import deeplabcut.generate_training_dataset.metadata as metadata +from deeplabcut.core.config.utils import get_yaml_dumper, get_yaml_loader +from deeplabcut.core.engine import Engine +from deeplabcut.utils import auxiliaryfunctions + +SHUFFLE_DATA = [ + {"name": "pJun17-t50s1", "index": 1, "train_fraction": 0.5, "split": 1, "engine": "torch"}, + {"name": "pJun17-t50s2", "index": 2, "train_fraction": 0.5, "split": 1, "engine": "tf"}, + {"name": "pJun17-t60s1", "index": 1, "train_fraction": 0.6, "split": 2, "engine": "torch"}, + {"name": "pJun17-t60s2", "index": 2, "train_fraction": 0.6, "split": 3, "engine": "torch"}, +] +SPLITS_DATA = { + 1: {"train": [0, 1], "test": [2, 3]}, + 2: {"train": [0, 1, 2], "test": [3, 4]}, + 3: {"train": [4, 3, 2], "test": [1, 0]}, +} + +BASE_SPLIT = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4)) +# Splits that should be equal to the base +EQ_SPLIT = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4)) +# Splits that should not be equal to the base +ADD_SPLIT = metadata.DataSplit(train_indices=(1, 2, 5), test_indices=(3, 4)) +ADD_SPLIT2 = metadata.DataSplit(train_indices=(1, 2), test_indices=(3, 4, 5)) +SUBS_SPLIT = metadata.DataSplit(train_indices=(1, 3), test_indices=(2, 4)) +DEL_SPLIT = metadata.DataSplit(train_indices=(1,), test_indices=(3, 4)) +DEL_SPLIT2 = metadata.DataSplit(train_indices=(1, 2), test_indices=(3,)) + +SHUFFLES = { + 1: metadata.ShuffleMetadata("pJun17-t50s1", 0.5, 1, Engine.PYTORCH, BASE_SPLIT), + 2: metadata.ShuffleMetadata("pJun17-t50s2", 0.5, 2, Engine.PYTORCH, ADD_SPLIT), + 3: metadata.ShuffleMetadata("pJun17-t50s3", 0.5, 3, Engine.TF, BASE_SPLIT), + 4: metadata.ShuffleMetadata("pJun17-t50s4", 0.5, 4, Engine.PYTORCH, DEL_SPLIT), +} + + +@pytest.mark.parametrize( + "data", + [ + { + "shuffles": {SHUFFLE_DATA[idx]["name"]: SHUFFLE_DATA[idx] for idx in [0, 1, 2]}, + "splits": {idx: SPLITS_DATA[idx] for idx in [1, 2]}, + }, + { + "shuffles": {SHUFFLE_DATA[idx]["name"]: SHUFFLE_DATA[idx] for idx in [0]}, + "splits": {idx: SPLITS_DATA[idx] for idx in [1, 2]}, + }, + ], +) +@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.""" + # write data to tmp file + cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) + with open(meta_path, "w") as f: + get_yaml_dumper().dump(data, f) + + print(cfg_path) + print(meta_path) + print(data["shuffles"]) + print(data["splits"]) + print() + + 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) + + trainset_meta = metadata.TrainingDatasetMetadata.load(str(cfg_path), load_splits=load_splits) + for s in trainset_meta.shuffles: + print(s) + + assert len(data["shuffles"]) == len(trainset_meta.shuffles) + + for s in trainset_meta.shuffles: + shuffle_in = data["shuffles"][s.name] + split_idx = data["splits"][shuffle_in["split"]] + assert s.train_fraction == shuffle_in["train_fraction"] + assert s.engine == Engine(shuffle_in["engine"]) + if load_splits: + assert s.split is not None + assert s.split.train_indices == tuple(split_idx["train"]) + assert s.split.test_indices == tuple(split_idx["test"]) + else: + assert s.split is None + s_with_split = s.load_split(cfg, trainset_dir) + assert s_with_split.split.train_indices == tuple(split_idx["train"]) + 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"}}, + }, + }, + { + "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"}, + }, + }, + }, + { + "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) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, data["shuffles"]) + print(trainset_meta) + + trainset_meta.save() + with open(meta_path) as f: + meta = get_yaml_loader().load(f) + print(data) + print(meta) + 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]]], +) +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, + ) + _create_doc_data(cfg, trainset_dir, s.train_fraction, s.index, train, test) + + trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple(shuffles)) + print(trainset_meta) + trainset_meta.save() + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + print(reloaded) + print() + + for s in trainset_meta.shuffles: + print(s) + print() + for s in reloaded.shuffles: + print(s) + print() + reloaded_with_splits = [s.load_split(cfg, trainset_dir) for s in reloaded.shuffles] + assert len(reloaded.shuffles) == len(trainset_meta.shuffles) + assert len(reloaded_with_splits) == len(trainset_meta.shuffles) + assert tuple(reloaded_with_splits) == trainset_meta.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_added = trainset_meta.add(SHUFFLES[2]) + assert len(trainset_meta.shuffles) == 1 + assert len(trainset_meta_added.shuffles) == 2 + assert trainset_meta_added.shuffles == (SHUFFLES[1], SHUFFLES[2]) + + +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_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 len(trainset_meta_added.shuffles) == len(trainset_meta_added_2.shuffles) + assert trainset_meta_added.shuffles == trainset_meta_added_2.shuffles + + +def test_add_shuffle_sorts_to_correct_order(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], SHUFFLES[3])) + trainset_meta_added = trainset_meta.add(SHUFFLES[2]) + assert len(trainset_meta.shuffles) == 2 + assert len(trainset_meta_added.shuffles) == 3 + 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("shuffle_to_add", [1, 2, 3, 4]) +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])) + 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=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) + 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])) + + +@pytest.mark.parametrize( + "split1, split2, equal", + [ + (BASE_SPLIT, EQ_SPLIT, True), + (BASE_SPLIT, ADD_SPLIT, False), + (BASE_SPLIT, ADD_SPLIT2, False), + (BASE_SPLIT, SUBS_SPLIT, False), + (BASE_SPLIT, DEL_SPLIT, False), + (BASE_SPLIT, DEL_SPLIT2, False), + ], +) +def test_data_split_equality(split1, split2, equal): + """Tests that equality functions as expected for DataSplits.""" + print(split1) + print(split2) + print(equal) + assert (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]): + """Tests that equality functions as expected for DataSplits.""" + with pytest.raises(RuntimeError): + 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)) + + with pytest.raises(RuntimeError): + metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(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}, + ), + ], +) +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) + print(trainset_dir) + for s in 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) + + trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) + print() + print(trainset_metadata) + assert len(trainset_metadata.shuffles) == len(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 + assert tuple(shuffle_data["train"]) == shuffle.split.train_indices + assert tuple(shuffle_data["test"]) == shuffle.split.test_indices + print() + + +def test_get_shuffle_engine_warns_when_metadata_get_fails_then_uses_model_folder(caplog): + """ValueError from metadata lookup is logged; engine is inferred from model folders.""" + caplog.set_level(logging.WARNING) + + cfg = { + "project_path": "/tmp/dlc-nonexistent-project-path", + "TrainingFraction": [0.95], + "Task": "t", + "date": "d", + "scorer": "s", + "iteration": 0, + } + + meta_path_mock = MagicMock() + meta_path_mock.exists.return_value = True + + training_meta_mock = MagicMock() + training_meta_mock.get.side_effect = ValueError("no shuffle for this index") + + with ( + patch.object(metadata.TrainingDatasetMetadata, "path", return_value=meta_path_mock), + patch.object(metadata.TrainingDatasetMetadata, "load", return_value=training_meta_mock), + patch.object(metadata, "find_engines_from_model_folders", return_value={Engine.PYTORCH}), + ): + engine = metadata.get_shuffle_engine(cfg, trainingsetindex=0, shuffle=1) + + assert engine == Engine.PYTORCH + assert "no shuffle for this index" in caplog.text + assert "Falling back to detecting the engine from model folders" in caplog.text + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.__post_init__ +# --------------------------------------------------------------------------- + + +def test_training_dataset_metadata_requires_sorted_shuffles(tmpdir): + """Constructor raises RuntimeError when shuffles are not sorted.""" + cfg, *_ = _create_project_with_config(tmpdir) + with pytest.raises(RuntimeError): + metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[2], SHUFFLES[1])) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.get +# --------------------------------------------------------------------------- + + +def test_get_returns_matching_shuffle(tmpdir): + """get() returns the correct ShuffleMetadata.""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + result = trainset_meta.get(trainset_index=0, index=1) + assert result == SHUFFLES[1] + + +def test_get_raises_when_trainset_index_out_of_bounds(tmpdir): + """get() raises ValueError when trainset_index >= len(TrainingFraction).""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + with pytest.raises(ValueError, match="out of bounds"): + trainset_meta.get(trainset_index=1, index=1) + + +def test_get_raises_when_shuffle_not_found(tmpdir): + """get() raises ValueError when no shuffle matches the given index.""" + cfg, *_ = _create_project_with_config(tmpdir) + cfg["TrainingFraction"] = [0.5] + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + + with pytest.raises(ValueError, match="Could not find"): + trainset_meta.get(trainset_index=0, index=99) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.save — lazy load_split branch +# --------------------------------------------------------------------------- + + +def test_save_loads_split_when_shuffle_has_no_split(tmpdir): + """save() calls load_split for shuffles where split is None.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + + # Build a shuffle with split=None — mimics a load(load_splits=False) result + shuffle_no_split = metadata.ShuffleMetadata( + name=SHUFFLES[1].name, + train_fraction=0.5, + index=1, + engine=Engine.PYTORCH, + split=None, + ) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (shuffle_no_split,)) + # Should not raise; save() must call load_split internally + trainset_meta.save() + + reloaded = metadata.TrainingDatasetMetadata.load(cfg, load_splits=True) + assert len(reloaded.shuffles) == 1 + assert reloaded.shuffles[0].split is not None + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.load +# --------------------------------------------------------------------------- + + +def test_load_raises_when_metadata_file_missing(tmpdir): + """load() raises FileNotFoundError when metadata.yaml does not exist.""" + cfg, *_ = _create_project_with_config(tmpdir) + with pytest.raises(FileNotFoundError): + metadata.TrainingDatasetMetadata.load(cfg) + + +# --------------------------------------------------------------------------- +# TrainingDatasetMetadata.create — empty trainset_path branch +# --------------------------------------------------------------------------- + + +def test_create_returns_empty_when_trainset_dir_missing(tmp_path): + """create() returns metadata with no shuffles when the trainset dir is absent.""" + cfg = { + "Task": "t", + "date": "d", + "scorer": "s", + "iteration": 0, + "project_path": str(tmp_path), + } + trainset_meta = metadata.TrainingDatasetMetadata.create(cfg) + assert len(trainset_meta.shuffles) == 0 + + +# --------------------------------------------------------------------------- +# update_metadata +# --------------------------------------------------------------------------- + + +def test_update_metadata_adds_shuffle(tmpdir): + """update_metadata adds a new shuffle and persists it.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + # Seed an existing metadata file with one shuffle + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=2, + engine=Engine.PYTORCH, + train_indices=[0, 1], + test_indices=[2, 3], + ) + + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + indices = [s.index for s in reloaded.shuffles] + assert 2 in indices + + +def test_update_metadata_overwrite(tmpdir): + """update_metadata with overwrite=True replaces an existing shuffle.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=1, + engine=Engine.TF, + train_indices=[0, 1], + test_indices=[2, 3], + overwrite=True, + ) + + reloaded = metadata.TrainingDatasetMetadata.load(cfg) + assert len(reloaded.shuffles) == 1 + assert reloaded.shuffles[0].engine == Engine.TF + + +def test_update_metadata_raises_without_overwrite(tmpdir): + """update_metadata raises RuntimeError when shuffle exists and overwrite=False.""" + cfg, _cfg_path, trainset_dir, _meta_path = _create_project_with_config(tmpdir) + _create_doc_data(cfg, trainset_dir, 0.5, 1, [0, 1], [2, 3]) + seed = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) + seed.save() + + with pytest.raises(RuntimeError): + metadata.update_metadata( + cfg, + train_fraction=0.5, + shuffle=1, + engine=Engine.PYTORCH, + train_indices=[0, 1], + test_indices=[2, 3], + overwrite=False, + ) + + +def _create_project_with_config( + tmp, + task: str = "example", + date: str = "Feb21", + scorer: str = "wayneRooney", + iteration: int = 0, + engine: str | None = None, +): + project_dir = tmp.mkdir("ex-ample-2024-02-21") + cfg = { + "Task": task, + "date": date, + "scorer": scorer, + "iteration": iteration, + "project_path": str(project_dir), + } + if engine is not None: + cfg["engine"] = engine + + cfg_path = project_dir.join("config.yaml") + with open(cfg_path, "w") as file: + get_yaml_dumper().dump(cfg, file) + + it = f"iteration-{iteration}" + dir_name = "UnaugmentedDataSet_" + task + date + trainset_dir = project_dir.mkdir("training-datasets").mkdir(it).mkdir(dir_name) + + meta_path = trainset_dir.join("metadata.yaml") + return cfg, cfg_path, trainset_dir, meta_path + + +def _create_doc_data( + cfg, + trainset_dir, + train_frac, + shuffle, + 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) diff --git a/tests/gui/conftest.py b/tests/gui/conftest.py new file mode 100644 index 0000000000..606119ac49 --- /dev/null +++ b/tests/gui/conftest.py @@ -0,0 +1,71 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Shared fixtures for GUI tests (headless Qt via pytest-qt).""" + +import os +import sys + +# Render off-screen so tests do not open windows; must be set before Qt loads. +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + + +@pytest.fixture +def write_project_config(): + """Writer for a minimal valid project config.yaml. + + ``project_path`` should be the parent of ``config_path`` so that + ``read_config`` does not repair (and rewrite) the file. + """ + + def _write(config_path, project_path, task: str = "demo", extra: str = "") -> None: + lines = [ + f"Task: {task}", + "scorer: tester", + "date: Jan1", + "multianimalproject: false", + f"project_path: {project_path.as_posix()}", + "bodyparts: [nose, tail]", + "video_sets: {}", + ] + if extra: + lines.append(extra) + config_path.write_text("\n".join(lines) + "\n") + + return _write + + +@pytest.fixture +def main_window(qapp, monkeypatch): + """A real MainWindow, constructed off-screen and torn down cleanly.""" + from PySide6 import QtWidgets + + from deeplabcut.gui.window import MainWindow + + # Keep QSettings written by save_settings() out of the user's real settings. + qapp.setOrganizationName("DeepLabCut-Tests") + qapp.setApplicationName("DLC-GUI-Tests") + + # closeEvent pops a blocking confirmation dialog; auto-confirm it in tests. + monkeypatch.setattr( + QtWidgets.QMessageBox, + "question", + lambda *args, **kwargs: QtWidgets.QMessageBox.Yes, + ) + + original_stdout = sys.stdout # MainWindow redirects stdout to its writer + window = MainWindow(qapp) + try: + yield window + finally: + window.close() + sys.stdout = original_stdout diff --git a/tests/gui/test_auto_update.py b/tests/gui/test_auto_update.py new file mode 100644 index 0000000000..c0c26adedc --- /dev/null +++ b/tests/gui/test_auto_update.py @@ -0,0 +1,141 @@ +import sys + +import pytest + +pytest.importorskip("PySide6") + +from deeplabcut.gui.utils import _build_update_commands, _package_specs_for_update + + +def test_package_specs_for_update_adds_gui_extra_to_deeplabcut(): + assert _package_specs_for_update(["deeplabcut"]) == ["deeplabcut[gui]"] + + +def test_package_specs_for_update_preserves_other_packages(): + assert _package_specs_for_update(["napari-deeplabcut"]) == ["napari-deeplabcut"] + + +def test_package_specs_for_update_handles_mixed_packages(): + assert _package_specs_for_update(["deeplabcut", "napari-deeplabcut"]) == [ + "deeplabcut[gui]", + "napari-deeplabcut", + ] + + +def test_package_specs_for_update_strips_whitespace(): + assert _package_specs_for_update([" deeplabcut ", " napari-deeplabcut "]) == [ + "deeplabcut[gui]", + "napari-deeplabcut", + ] + + +@pytest.mark.parametrize( + ("available_installers", "expected_backends"), + [ + ({}, ["pip"]), + ({"uv": "/mock/bin/uv"}, ["uv", "pip"]), + ], +) +def test_build_update_commands_backend_order(monkeypatch, available_installers, expected_backends): + def fake_which(name): + return available_installers.get(name) + + monkeypatch.setattr("deeplabcut.gui.utils.shutil.which", fake_which) + + commands = _build_update_commands(["deeplabcut", "napari-deeplabcut"]) + + assert [backend for backend, _program, _args in commands] == expected_backends + + +def test_build_update_commands_uses_uv_when_available(monkeypatch): + monkeypatch.setattr( + "deeplabcut.gui.utils.shutil.which", + lambda name: "/mock/bin/uv" if name == "uv" else None, + ) + + commands = _build_update_commands(["deeplabcut", "napari-deeplabcut"]) + + assert commands == [ + ( + "uv", + "/mock/bin/uv", + [ + "pip", + "install", + "--python", + sys.executable, + "-U", + "deeplabcut[gui]", + "napari-deeplabcut", + ], + ), + ( + "pip", + sys.executable, + [ + "-m", + "pip", + "install", + "-U", + "deeplabcut[gui]", + "napari-deeplabcut", + ], + ), + ] + + +def test_build_update_commands_uses_uv_then_pip(monkeypatch): + installers = {"uv": "/mock/bin/uv"} + + monkeypatch.setattr( + "deeplabcut.gui.utils.shutil.which", + lambda name: installers.get(name), + ) + + commands = _build_update_commands(["deeplabcut"]) + + assert commands == [ + ( + "uv", + "/mock/bin/uv", + [ + "pip", + "install", + "--python", + sys.executable, + "-U", + "deeplabcut[gui]", + ], + ), + ( + "pip", + sys.executable, + [ + "-m", + "pip", + "install", + "-U", + "deeplabcut[gui]", + ], + ), + ] + + +def test_build_update_commands_always_has_pip_fallback(monkeypatch): + monkeypatch.setattr("deeplabcut.gui.utils.shutil.which", lambda _name: None) + + commands = _build_update_commands(["deeplabcut"]) + + assert commands == [ + ( + "pip", + sys.executable, + [ + "-m", + "pip", + "install", + "-U", + "deeplabcut[gui]", + ], + ) + ] diff --git a/tests/gui/test_config_editor.py b/tests/gui/test_config_editor.py new file mode 100644 index 0000000000..202d8b1721 --- /dev/null +++ b/tests/gui/test_config_editor.py @@ -0,0 +1,57 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for the raw YAML tree editor (ConfigEditor).""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + +from deeplabcut.gui.widgets import ConfigEditor +from deeplabcut.utils import auxiliaryfunctions + + +def test_editor_displays_project_config_as_tree(qtbot, tmp_path, write_project_config): + """Regression: read_config returns a ProjectConfig model, which the tree + editor cannot display; the editor must read the raw YAML dict instead.""" + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + + editor = ConfigEditor(str(config_path)) + qtbot.addWidget(editor) + + assert isinstance(editor.cfg, dict) + + root = editor.viewer.tree.invisibleRootItem() + keys = {root.child(i).text(0) for i in range(root.childCount())} + assert {"Task", "bodyparts", "multianimalproject"} <= keys + + +def test_editor_opens_config_that_fails_validation(qtbot, tmp_path, write_project_config): + """The editor is a recovery tool: it must open invalid configs so the + user can repair them.""" + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path, extra="not_a_dlc_setting: 1") + + editor = ConfigEditor(str(config_path)) # must not raise + qtbot.addWidget(editor) + + assert editor.cfg["not_a_dlc_setting"] == 1 + + +def test_editor_saves_edits_back_to_disk(qtbot, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + + editor = ConfigEditor(str(config_path)) + qtbot.addWidget(editor) + editor.cfg["colormap"] = "viridis" + editor.accept() + + on_disk = auxiliaryfunctions.read_plainconfig(str(config_path)) + assert on_disk["colormap"] == "viridis" diff --git a/tests/gui/test_config_errors.py b/tests/gui/test_config_errors.py new file mode 100644 index 0000000000..1a9e0a0147 --- /dev/null +++ b/tests/gui/test_config_errors.py @@ -0,0 +1,99 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for user-facing formatting of configuration errors.""" + +import pytest + +pytest.importorskip("PySide6") # deeplabcut.gui imports qtpy at package level + +from pathlib import Path + +from pydantic import ValidationError + +from deeplabcut.core.config import ProjectConfig +from deeplabcut.gui.dialogs.config_errors import ( + CONFIG_LOAD_ERRORS, + format_config_error, +) + +CONFIG_PATH = "C:/projects/demo/config.yaml" +# The report renders the path in native form (backslashes on Windows). +CONFIG_PATH_DISPLAY = str(Path(CONFIG_PATH)) + + +def _make_validation_error(cfg: dict) -> ValidationError: + with pytest.raises(ValidationError) as exc_info: + ProjectConfig.from_dict(cfg) + return exc_info.value + + +def test_validation_error_report_names_the_field(): + error = _make_validation_error({"multianimalproject": "banana"}) + + report = format_config_error(CONFIG_PATH, error) + + assert report.title == "Invalid project configuration" + assert "1 problem" in report.summary + assert CONFIG_PATH_DISPLAY in report.details + assert "multianimalproject" in report.details + assert "'banana'" in report.details # the received value is shown + assert report.technical_details # raw pydantic message preserved + + +def test_extra_forbidden_gets_friendly_message(): + error = _make_validation_error({"not_a_dlc_setting": 1}) + + report = format_config_error(CONFIG_PATH, error) + + assert "not_a_dlc_setting" in report.details + assert "not supported by the installed DeepLabCut version" in report.details + + +def test_multiple_errors_are_all_listed(): + error = _make_validation_error({"not_a_dlc_setting": 1, "multianimalproject": "banana"}) + + report = format_config_error(CONFIG_PATH, error) + + assert "2 problems" in report.summary + assert "not_a_dlc_setting" in report.details + assert "multianimalproject" in report.details + + +def test_file_not_found_report(): + report = format_config_error(CONFIG_PATH, FileNotFoundError(CONFIG_PATH)) + + assert report.title == "Project configuration not found" + assert CONFIG_PATH_DISPLAY in report.details + + +def test_permission_error_report(): + report = format_config_error(CONFIG_PATH, PermissionError("denied")) + + assert report.title == "Cannot read project configuration" + assert "permission" in report.summary.lower() + + +def test_generic_error_report_includes_message(): + report = format_config_error(CONFIG_PATH, ValueError("config is empty or null")) + + assert report.title == "Cannot load project configuration" + assert "config is empty or null" in report.details + + +@pytest.mark.parametrize( + "error", + [ + FileNotFoundError("x"), + PermissionError("x"), + OSError("x"), + TypeError("x"), + ValueError("x"), + ], +) +def test_config_load_errors_cover_common_failures(error): + assert isinstance(error, CONFIG_LOAD_ERRORS) diff --git a/tests/gui/test_config_file_monitor.py b/tests/gui/test_config_file_monitor.py new file mode 100644 index 0000000000..393a38e9a6 --- /dev/null +++ b/tests/gui/test_config_file_monitor.py @@ -0,0 +1,80 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for the external-edit watcher on the project configuration.""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + +from PySide6 import QtWidgets + +from deeplabcut.gui.config_file_monitor import ConfigFileMonitor + + +@pytest.fixture +def status_bar(qtbot): + bar = QtWidgets.QStatusBar() + qtbot.addWidget(bar) + return bar + + +def _make_monitor(status_bar, config_path, on_reload=lambda: True): + monitor = ConfigFileMonitor(status_bar, on_reload=on_reload, debounce_ms=50) + monitor.set_path(str(config_path)) + monitor.mark_current() + return monitor + + +def test_external_edit_offers_reload(qtbot, tmp_path, status_bar): + config = tmp_path / "config.yaml" + config.write_text("Task: test\n") + monitor = _make_monitor(status_bar, config) + assert monitor._reload_button.isHidden() + + config.write_text("Task: test\nedited: true\n") + + qtbot.waitUntil(lambda: not monitor._reload_button.isHidden(), timeout=5000) + + +def test_mark_current_dismisses_reload_offer(qtbot, tmp_path, status_bar): + config = tmp_path / "config.yaml" + config.write_text("Task: test\n") + monitor = _make_monitor(status_bar, config) + + config.write_text("Task: changed\n") + qtbot.waitUntil(lambda: not monitor._reload_button.isHidden(), timeout=5000) + + monitor.mark_current() # simulates a successful reload + assert monitor._reload_button.isHidden() + + +def test_set_path_resets_pending_notification(qtbot, tmp_path, status_bar): + config = tmp_path / "config.yaml" + config.write_text("Task: test\n") + monitor = _make_monitor(status_bar, config) + + config.write_text("Task: changed\n") + qtbot.waitUntil(lambda: not monitor._reload_button.isHidden(), timeout=5000) + + other = tmp_path / "other.yaml" + other.write_text("Task: other\n") + monitor.set_path(str(other)) + + assert monitor._reload_button.isHidden() + + +def test_reload_button_triggers_callback(qtbot, tmp_path, status_bar): + config = tmp_path / "config.yaml" + config.write_text("Task: test\n") + reloads = [] + monitor = _make_monitor(status_bar, config, on_reload=lambda: reloads.append(True)) + + monitor._reload_button.click() + + assert reloads == [True] diff --git a/tests/gui/test_main_window_config.py b/tests/gui/test_main_window_config.py new file mode 100644 index 0000000000..3220af2700 --- /dev/null +++ b/tests/gui/test_main_window_config.py @@ -0,0 +1,186 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Functional tests for MainWindow config caching and error recovery. + +These construct a real (off-screen) MainWindow but stub out ``add_tabs`` and +the error dialog, so no DLC project on disk and no user interaction is needed. +""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + +from PySide6 import QtWidgets + +pytestmark = pytest.mark.functional + + +class TestCfgCaching: + def test_cfg_is_cached_between_accesses(self, main_window, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + + main_window.config = str(config_path) + + first = main_window.cfg + assert first is not None + assert first is main_window.cfg # same validated snapshot + + def test_external_edit_does_not_silently_reload(self, main_window, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + main_window.config = str(config_path) + first = main_window.cfg + + write_project_config(config_path, tmp_path, task="edited") + + # The GUI keeps operating on the loaded snapshot until an explicit reload. + assert main_window.cfg is first + assert main_window.cfg.Task == "demo" + + def test_assigning_config_invalidates_cache(self, main_window, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + main_window.config = str(config_path) + first = main_window.cfg + + write_project_config(config_path, tmp_path, task="edited") + main_window.config = str(config_path) # explicit reload boundary + + reloaded = main_window.cfg + assert reloaded is not first + assert reloaded.Task == "edited" + + def test_cfg_is_none_without_a_project(self, main_window): + main_window.config = None + assert main_window.cfg is None + + +class TestRecoveryLoop: + """_build_project_ui_from_current_config must never crash the window.""" + + @pytest.fixture + def stubbed_window(self, main_window): + main_window._built_tabs = [] + main_window.add_tabs = lambda: main_window._built_tabs.append(True) + return main_window + + def test_cancel_leaves_welcome_page(self, stubbed_window, tmp_path, write_project_config): + from deeplabcut.gui.window import ConfigErrorAction + + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path, extra="not_a_dlc_setting: 1") + + handled = [] + + def cancel(error): + handled.append(error) + return ConfigErrorAction.CANCEL + + stubbed_window._handle_config_error = cancel + stubbed_window.config = str(config_path) + + assert stubbed_window._build_project_ui_from_current_config() is False + assert len(handled) == 1 + assert stubbed_window._built_tabs == [] + + def test_retry_succeeds_after_user_fixes_config(self, stubbed_window, tmp_path, write_project_config): + from deeplabcut.gui.window import ConfigErrorAction + + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path, extra="not_a_dlc_setting: 1") + + def fix_file_and_retry(error): + write_project_config(config_path, tmp_path, task="repaired") + return ConfigErrorAction.RETRY + + stubbed_window._handle_config_error = fix_file_and_retry + stubbed_window.config = str(config_path) + + assert stubbed_window._build_project_ui_from_current_config() is True + assert stubbed_window._built_tabs == [True] + # Each retry re-reads from disk, so the repaired file is what got loaded. + assert stubbed_window.cfg.Task == "repaired" + + def test_valid_config_builds_tabs_without_error_handling(self, stubbed_window, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + + def unexpected(error): # pragma: no cover - should not run + raise AssertionError(f"error handler should not be called: {error}") + + stubbed_window._handle_config_error = unexpected + stubbed_window.config = str(config_path) + + assert stubbed_window._build_project_ui_from_current_config() is True + assert stubbed_window._built_tabs == [True] + + +class TestConfigCacheInvalidation: + def test_invalidate_drops_cache_for_next_access(self, main_window, tmp_path, write_project_config): + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + main_window.config = str(config_path) + + first = main_window.cfg + assert first is not None + + write_project_config(config_path, tmp_path, task="changed-on-disk") + main_window.invalidate_config_cache() + reloaded = main_window.cfg + + assert reloaded is not first + assert reloaded.Task == "changed-on-disk" + + def test_invalidate_is_idempotent(self, main_window): + # Must not raise even with no config loaded. + main_window.config = None + main_window.invalidate_config_cache() + assert main_window.cfg is None + + +class TestReloadTimer: + def test_named_timer_triggers_reload(self, qapp, qtbot, tmp_path, write_project_config, monkeypatch): + """Verify the _reload_timer in ManageProject fires reload_project_config.""" + from deeplabcut.gui.tabs.manage_project import ManageProject + + # Keep QSettings out of the real user settings. + qapp.setOrganizationName("DeepLabCut-Tests") + qapp.setApplicationName("DLC-GUI-Tests") + monkeypatch.setattr( + QtWidgets.QMessageBox, + "question", + lambda *args, **kwargs: QtWidgets.QMessageBox.Yes, + ) + + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + + from deeplabcut.gui.window import MainWindow + + window = MainWindow(qapp) + try: + window.config = str(config_path) + + reloads = [] + monkeypatch.setattr( + window, + "reload_project_config", + lambda: reloads.append(True), + ) + + tab = ManageProject(window, window, "") + tab._reload_timer.start() + + # The timer has interval=0 so it fires on the next event loop tick. + qtbot.wait(50) + + assert len(reloads) == 1 + finally: + window.close() diff --git a/tests/gui/test_selected_shuffle_display.py b/tests/gui/test_selected_shuffle_display.py new file mode 100644 index 0000000000..b11e68c007 --- /dev/null +++ b/tests/gui/test_selected_shuffle_display.py @@ -0,0 +1,94 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for the shuffle info display degrading gracefully on bad shuffles.""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + +import PySide6.QtCore as QtCore + +from deeplabcut.core.engine import Engine +from deeplabcut.gui.displays.selected_shuffle_display import SelectedShuffleDisplay + + +class FakeRoot(QtCore.QObject): + """Minimal stand-in for MainWindow as seen by SelectedShuffleDisplay.""" + + shuffle_change = QtCore.Signal(int) + engine_change = QtCore.Signal(object) + shuffle_created = QtCore.Signal(int) + + def __init__(self, pose_cfg_path: str, raise_error: Exception | None = None): + super().__init__() + self.shuffle_value = 1 + self.engine = Engine.PYTORCH + self._pose_cfg_path = pose_cfg_path + self._raise_error = raise_error + + @property + def pose_cfg_path(self) -> str: + if self._raise_error is not None: + raise self._raise_error + return self._pose_cfg_path + + +def test_missing_pose_cfg_shows_error_instead_of_crashing(qtbot, tmp_path): + missing = tmp_path / "missing" / "pose_cfg.yaml" + display = SelectedShuffleDisplay(FakeRoot(str(missing))) + qtbot.addWidget(display) + + assert display.pose_cfg is None + assert "was not created" in display._label.text() + + +def test_unresolvable_shuffle_shows_error(qtbot, tmp_path): + root = FakeRoot(str(tmp_path), raise_error=ValueError("no such shuffle")) + display = SelectedShuffleDisplay(root) + qtbot.addWidget(display) + + assert display.pose_cfg is None + assert "Failed to read shuffle 1" in display._label.text() + + +def test_valid_pose_cfg_is_displayed(qtbot, tmp_path): + pose_cfg_path = tmp_path / "pytorch_config.yaml" + pose_cfg_path.write_text("net_type: resnet_50\nmethod: TD\n") + + display = SelectedShuffleDisplay(FakeRoot(str(pose_cfg_path))) + qtbot.addWidget(display) + + assert display.pose_cfg == {"net_type": "resnet_50", "method": "TD"} + assert "resnet_50" in display._label.text() + assert "top-down" in display._label.text() + + +def test_pose_cfg_without_method_key_does_not_crash(qtbot, tmp_path): + """Regression: pose_cfg.get('method').lower() raised AttributeError.""" + pose_cfg_path = tmp_path / "pytorch_config.yaml" + pose_cfg_path.write_text("net_type: resnet_50\n") + + display = SelectedShuffleDisplay(FakeRoot(str(pose_cfg_path))) + qtbot.addWidget(display) + + assert display.pose_cfg == {"net_type": "resnet_50"} + assert "top-down" not in display._label.text() + + +def test_pose_cfg_signal_carries_none(qtbot, tmp_path): + """Regression: the signal was declared Signal(dict) and mangled None.""" + missing = tmp_path / "missing" / "pose_cfg.yaml" + display = SelectedShuffleDisplay(FakeRoot(str(missing))) + qtbot.addWidget(display) + + received = [] + display.pose_cfg_signal.connect(received.append) + display.pose_cfg = None + + assert received == [None] diff --git a/tests/gui/test_task_error.py b/tests/gui/test_task_error.py new file mode 100644 index 0000000000..98b5efa375 --- /dev/null +++ b/tests/gui/test_task_error.py @@ -0,0 +1,81 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for MainWindow.show_task_error dialog rendering.""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + + +from pydantic import ValidationError +from PySide6 import QtWidgets + +from deeplabcut.core.config import ProjectConfig + + +def _make_validation_error(cfg: dict) -> ValidationError: + with pytest.raises(ValidationError) as exc_info: + ProjectConfig.from_dict(cfg) + return exc_info.value + + +class TestShowTaskError: + @pytest.fixture + def patched_messagebox(self, monkeypatch): + """Intercept every QMessageBox so show_task_error never blocks.""" + captured = {} + + class _Box(QtWidgets.QMessageBox): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + captured["instance"] = self + + def exec(self): + captured["exec_called"] = True + return 0 + + monkeypatch.setattr(QtWidgets, "QMessageBox", _Box) + return captured + + def test_generic_error_shows_task_failed_dialog( + self, main_window, tmp_path, write_project_config, patched_messagebox + ): + """Non-config exceptions get a generic 'Task failed' dialog.""" + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + main_window.config = str(config_path) + + main_window.show_task_error(ValueError("config is empty or null")) + + box = patched_messagebox["instance"] + assert box.windowTitle() == "Task failed" + assert box.text() == "DeepLabCut could not complete the task." + assert "config is empty or null" in box.informativeText() + + button_texts = [b.text() for b in box.buttons()] + assert "Open configuration" not in button_texts + + def test_config_validation_error_shows_open_button( + self, main_window, tmp_path, write_project_config, patched_messagebox + ): + """Config validation errors show an 'Open configuration' button.""" + config_path = tmp_path / "config.yaml" + write_project_config(config_path, tmp_path) + main_window.config = str(config_path) + + error = _make_validation_error({"multianimalproject": "banana"}) + + main_window.show_task_error(error, config_path=str(config_path)) + + box = patched_messagebox["instance"] + assert box.windowTitle() == "Invalid project configuration" + assert "multianimalproject" in box.informativeText() + + button_texts = [b.text() for b in box.buttons()] + assert "Open configuration" in button_texts diff --git a/tests/gui/test_worker.py b/tests/gui/test_worker.py new file mode 100644 index 0000000000..9953976035 --- /dev/null +++ b/tests/gui/test_worker.py @@ -0,0 +1,75 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for worker-thread task execution and error marshalling.""" + +import pytest + +pytest.importorskip("PySide6") +pytest.importorskip("pytestqt") + +from deeplabcut.gui.utils import CaptureWorker, Worker, move_to_separate_thread + + +def _boom(): + raise ZeroDivisionError("boom") + + +def test_worker_emits_finished_on_success(qtbot): + calls = [] + worker = Worker(lambda: calls.append(True)) + + with qtbot.waitSignal(worker.finished, timeout=1000): + worker.run() + + assert calls == [True] + + +def test_worker_emits_error_and_finished_on_exception(qtbot): + """Regression: a raising task used to kill the worker without any signal, + leaving progress bars spinning and buttons disabled forever.""" + worker = Worker(_boom) + errors = [] + worker.error.connect(errors.append) + + with qtbot.waitSignal(worker.finished, timeout=1000): + worker.run() + + assert len(errors) == 1 + assert isinstance(errors[0], ZeroDivisionError) + + +def test_capture_worker_captures_outputs(): + worker = CaptureWorker(lambda: "result") + worker.run() + assert worker.outputs == "result" + + +def test_capture_worker_outputs_none_on_error(qtbot): + worker = CaptureWorker(_boom) + errors = [] + worker.error.connect(errors.append) + + with qtbot.waitSignal(worker.finished, timeout=1000): + worker.run() + + assert worker.outputs is None + assert len(errors) == 1 + + +def test_thread_quits_after_exception(qtbot): + """Regression: the QThread must stop even when the task raises.""" + worker, thread = move_to_separate_thread(_boom) + errors = [] + worker.error.connect(errors.append) + + with qtbot.waitSignal(thread.finished, timeout=3000): + thread.start() + + qtbot.waitUntil(thread.isFinished, timeout=3000) + assert len(errors) == 1 + assert isinstance(errors[0], ZeroDivisionError) diff --git a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py new file mode 100644 index 0000000000..253841df54 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py @@ -0,0 +1,470 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from dataclasses import dataclass +from unittest.mock import Mock, patch + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.apis as apis +import deeplabcut.pose_estimation_pytorch.data as data + +PREDICT = Mock() + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize("num_individuals", [1, 2, 5]) +@pytest.mark.parametrize( + "bodyparts, error", + [ + (["nose", "left_ear"], [5, 10]), + (["nose", "left_ear", "right_ear"], [2, 3, 4]), + ], +) +def test_evaluate_basic( + num_individuals: int, + bodyparts: list[str], + error: list[float], +) -> None: + print() + gt, pred = generate_data(1, num_individuals, len(bodyparts), error) + + pose_runner = Mock() + + PREDICT.return_value = {img: {"bodyparts": pose} for img, pose in pred.items()} + loader = build_mock_loader(gt, num_individuals, bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test") + print("results", results) + np.testing.assert_almost_equal(results["rmse"], np.mean(error)) + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize("num_individuals", [1, 2, 5]) +@pytest.mark.parametrize( + "bodyparts, error", + [ + (["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, + bodyparts: list[str], + error: list[float], + unique_bodyparts: list[str], + unique_error: list[float], +) -> None: + 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) + + pose_runner = Mock() + PREDICT.return_value = { + 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) + 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])) + print(num_individuals) + print(error) + print(idv_errors) + print(unique_error) + print(np.concatenate([idv_errors, unique_error])) + print(expected_rmse) + print("results", results) + np.testing.assert_almost_equal(results["rmse"], expected_rmse) + + +@dataclass +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,) + comparison_bodyparts: str | list[str] | None = None + expected_error: float = (2 + 5 + 10) / 3 + + def num_bpt(self) -> int: + return len(self.bodyparts) + + def num_unique(self) -> int: + return len(self.unique_bodyparts) + + +@patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) +@pytest.mark.parametrize( + "cfg", + [ + CompTestConfig(comparison_bodyparts=None), + CompTestConfig(comparison_bodyparts="all"), + CompTestConfig(comparison_bodyparts=["nose", "left_ear", "top_left"]), + CompTestConfig(num_individuals=2, expected_error=(2 + 5 + 5 + 10 + 10) / 5), + CompTestConfig(comparison_bodyparts="nose", expected_error=5), + CompTestConfig(comparison_bodyparts=["nose"], expected_error=5), + CompTestConfig(comparison_bodyparts=["left_ear"], expected_error=10), + CompTestConfig(comparison_bodyparts=["nose", "left_ear"], expected_error=7.5), + CompTestConfig(comparison_bodyparts="top_left", expected_error=2), + CompTestConfig(comparison_bodyparts=["top_left"], expected_error=2), + CompTestConfig( + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + comparison_bodyparts=["a", "b", "c"], + expected_error=4, + ), + CompTestConfig( + num_individuals=1, + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + comparison_bodyparts=["nose", "a", "b", "c"], + expected_error=(5.0 + 3.0 + 4.0 + 5.0) / 4, + ), + CompTestConfig( + num_individuals=7, + unique_bodyparts=("a", "b", "c"), + unique_error=(3.0, 4.0, 5.0), + 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() + num_images = 5 + gt, pred = generate_data(num_images, cfg.num_individuals, cfg.num_bpt(), cfg.error) + gt_unique, pred_unique = generate_data(num_images, 1, cfg.num_unique(), cfg.unique_error) + + pose_runner = Mock() + PREDICT.return_value = { + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() + } + loader = build_mock_loader( + gt, + cfg.num_individuals, + cfg.bodyparts, + gt_unique=gt_unique, + unique=cfg.unique_bodyparts, + ) + results, preds = apis.evaluate( + pose_runner, + loader, + mode="test", + comparison_bodyparts=cfg.comparison_bodyparts, + ) + print(cfg) + print("results", results) + np.testing.assert_almost_equal(results["rmse"], cfg.expected_error) + + +@dataclass +class KeypointData: + img: int + idv: int + bodypart: str + gt: tuple[float, float] + pred: tuple[float, float] + score: float + + 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() + + +@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]], +) +@pytest.mark.parametrize( + "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), + KeypointData(img=0, idv=0, bodypart="c", gt=(20, 20), pred=(20, 22), score=0.5), + ], + [ + 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.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 30), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=1, bodypart="a", gt=(40, 10), pred=(41, 10), score=0.7), + 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], + keypoints: list[KeypointData], +) -> None: + print() + + images = {d.image() for d in keypoints} + individuals = list({d.idv for d in keypoints if d.idv != -1}) + bodyparts = list({d.bodypart for d in keypoints if d.idv != -1}) + unique_bodyparts = list({d.bodypart for d in keypoints if d.idv == -1}) + + num_idv = len(individuals) + num_bodyparts = len(bodyparts) + len(unique_bodyparts) + + gt, pred = {}, {} + for img in images: + gt[img] = np.zeros((num_idv, num_bodyparts, 3)) + pred[img] = np.zeros((num_idv, num_bodyparts, 3)) + + errors = [] + errors_cutoff = [] + for kpt in keypoints: + img = kpt.image() + bpt = bodyparts.index(kpt.bodypart) + + gt[img][kpt.idv, bpt, :2] = kpt.gt + gt[img][kpt.idv, bpt, 2] = 2 + pred[img][kpt.idv, bpt, :2] = kpt.pred + pred[img][kpt.idv, bpt, 2] = kpt.score + + if isinstance(pcutoff, list): + bpt_cutoff = pcutoff[bpt] + else: + bpt_cutoff = pcutoff + + errors.append(kpt.error()) + if kpt.score >= bpt_cutoff: + errors_cutoff.append(kpt.error()) + + print(errors) + print(errors_cutoff) + + pose_runner = Mock() + PREDICT.return_value = {img: {"bodyparts": pose} for img, pose in pred.items()} + loader = build_mock_loader(gt, num_idv, bodyparts) + results, preds = apis.evaluate(pose_runner, loader, mode="test", pcutoff=pcutoff) + print("results", results) + 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)) + + +@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, 0.4, 0.6], + [0.25, 0.43, 0.61, 0.46, 0.92], + [0.12, 0.15, 0.92, 0.97, 0.85], + [0.92, 0.97, 0.85, 0.12, 0.15], + ], +) +@pytest.mark.parametrize( + "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), + KeypointData(img=0, idv=0, bodypart="c", gt=(20, 20), pred=(20, 22), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(20, 20), pred=(20, 22), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.3), + ], + [ + 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.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 30), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=1, bodypart="a", gt=(40, 10), pred=(41, 10), score=0.7), + 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), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.9), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(8, 13), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 27), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=0, bodypart="c", gt=(30, 36), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(20, 20), pred=(20, 22), score=0.9), + KeypointData(img=1, idv=0, bodypart="a", gt=(15, 20), pred=(41, 10), score=0.7), + KeypointData(img=1, idv=0, bodypart="b", gt=(20, 12), pred=(49, 20), score=0.5), + KeypointData(img=1, idv=0, bodypart="c", gt=(17, 32), pred=(58, 20), score=0.2), + KeypointData(img=1, idv=-1, bodypart="u1", gt=(37, 4), pred=(3, 3), score=0.7), + KeypointData(img=1, idv=-1, bodypart="u2", gt=(12, 6), pred=(20, 22), score=0.9), + ], + [ + KeypointData(img=0, idv=0, bodypart="a", gt=(8, 13), pred=(11, 10), score=0.7), + KeypointData(img=0, idv=0, bodypart="b", gt=(20, 27), pred=(21, 20), score=0.5), + KeypointData(img=0, idv=-1, bodypart="u1", gt=(30, 36), pred=(30, 32), score=0.2), + KeypointData(img=0, idv=-1, bodypart="u2", gt=(2, 3), pred=(3, 3), score=0.7), + KeypointData(img=0, idv=-1, bodypart="u3", gt=(20, 20), pred=(20, 22), score=0.9), + KeypointData(img=1, idv=0, bodypart="a", gt=(15, 20), pred=(41, 10), score=0.7), + KeypointData(img=1, idv=0, bodypart="b", gt=(20, 12), pred=(49, 20), score=0.5), + 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], + keypoints: list[KeypointData], +) -> None: + print() + + images = {d.image() for d in keypoints} + individuals = list({d.idv for d in keypoints if d.idv != -1}) + bodyparts = list({d.bodypart for d in keypoints if d.idv != -1}) + unique_bodyparts = list({d.bodypart for d in keypoints if d.idv == -1}) + + num_idv = len(individuals) + num_bodyparts = len(bodyparts) + num_unique = len(unique_bodyparts) + + gt, pred, gt_unique, pred_unique = {}, {}, {}, {} + for img in images: + gt[img] = np.zeros((num_idv, num_bodyparts, 3)) + pred[img] = np.zeros((num_idv, num_bodyparts, 3)) + gt_unique[img] = np.zeros((1, num_unique, 3)) + pred_unique[img] = np.zeros((1, num_unique, 3)) + + errors, errors_cutoff = [], [] + for kpt in keypoints: + img = kpt.image() + if kpt.idv == -1: + idv, bpt = 0, unique_bodyparts.index(kpt.bodypart) + pcutoff_idx = bpt + len(bodyparts) # offset by number of bodyparts + gt_data, pred_data = gt_unique[img], pred_unique[img] + else: + idv, bpt = kpt.idv, bodyparts.index(kpt.bodypart) + pcutoff_idx = bpt + gt_data, pred_data = gt[img], pred[img] + + gt_data[idv, bpt, :2] = kpt.gt + gt_data[idv, bpt, 2] = 2 + pred_data[idv, bpt, :2] = kpt.pred + pred_data[idv, bpt, 2] = kpt.score + + if isinstance(pcutoff, list): + bpt_cutoff = pcutoff[pcutoff_idx] + else: + bpt_cutoff = pcutoff + + errors.append(kpt.error()) + if kpt.score >= bpt_cutoff: + errors_cutoff.append(kpt.error()) + + print(errors) + print(errors_cutoff) + + pose_runner = Mock() + PREDICT.return_value = { + 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) + + print("results", results) + 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)) + + +def generate_data( + num_images: int, + num_individuals: int, + num_bodyparts: int, + error: list[float] | tuple[float, ...] | np.ndarray, + cutoffs: list[float] | tuple[float, ...] | np.ndarray | None = None, + error_cutoff: list[float] | tuple[float, ...] | np.ndarray | None = None, +) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]: + num_elems = num_individuals * num_bodyparts + shape = num_individuals, num_bodyparts, 3 + error = np.asarray(error) + coord_error = (np.sqrt(2) / 2) * error + + gt, pred = {}, {} + for img in range(num_images): + gt_pose = 100 * np.arange(3 * num_elems, dtype=float).reshape(shape) + gt_pose[..., 2] = 2 + gt[f"img_{img:04d}.png"] = gt_pose + + pred_pose = np.ones(shape, dtype=float) + pred_pose[..., :2] = gt_pose[..., :2] + pred_pose[:, :, 0] += coord_error + pred_pose[:, :, 1] += coord_error + pred[f"img_{img:04d}.png"] = pred_pose + + if error_cutoff is not None and cutoffs is not None: + for img in range(num_images): + gt_pose = 100 * np.arange(3 * num_elems, dtype=float).reshape(shape) + gt_pose[..., 2] = 2 + gt[f"img_{num_images + img:04d}.png"] = gt_pose + + pred_pose = np.ones(shape, dtype=float) + pred_pose[..., :2] = gt_pose[..., :2] + pred_pose[..., 2] = cutoffs + pred_pose[:, :, 0] += coord_error + pred_pose[:, :, 1] += coord_error + pred[f"img_{num_images + img:04d}.png"] = pred_pose + + return gt, pred + + +def build_mock_loader( + gt: dict[str, np.ndarray], + num_individuals: int, + bodyparts: list[str] | tuple[str, ...], + gt_unique: dict[str, np.ndarray] | None = None, + unique: list[str] | tuple[str, ...] | None = None, +) -> Mock: + if unique is None: + unique = [] + + def _gt(mode: str, unique_bodypart: bool = False) -> dict[str, np.ndarray]: + if unique_bodypart: + print("LOADING UNIQUE GT") + return gt_unique + print("LOADING GT") + return gt + + individuals = [f"animal_{i:03d}" for i in range(num_individuals)] + loader = Mock() + loader.get_dataset_parameters.return_value = data.PoseDatasetParameters( + bodyparts=bodyparts, + unique_bpts=unique, + individuals=individuals, + ) + loader.ground_truth_keypoints = _gt + loader.model_cfg = { + "metadata": { + "bodyparts": bodyparts, + "unique_bodyparts": unique, + "individuals": individuals, + "with_identity": False, + }, + "train_settings": {}, + } + return loader diff --git a/tests/pose_estimation_pytorch/apis/test_apis_export.py b/tests/pose_estimation_pytorch/apis/test_apis_export.py new file mode 100644 index 0000000000..4e18e8aad9 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_export.py @@ -0,0 +1,324 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests exporting models.""" + +import copy +import shutil +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest +import torch +from ruamel.yaml.scalarstring import SingleQuotedScalarString as SQS + +import deeplabcut.pose_estimation_pytorch.apis.export as export +from deeplabcut.core.config import ProjectConfig +from deeplabcut.pose_estimation_pytorch import Task +from deeplabcut.pose_estimation_pytorch.config.pose import PoseConfig +from deeplabcut.pose_estimation_pytorch.runners.snapshots import Snapshot + + +def _minimal_pose_config(tmp_path: Path, *, resume_from: str = "/abs/snapshot.pt") -> PoseConfig: + project_config = ProjectConfig( + project_path=tmp_path, + bodyparts=["nose"], + individuals=["mouse"], + multianimalproject=False, + ) + pose_config = PoseConfig.build( + project_config, + tmp_path / "pytorch_config.yaml", + top_down=False, + net_type="resnet_50", + ) + pose_config.resume_training_from = resume_from + return pose_config + + +@pytest.fixture() +def project_dir(tmp_path_factory) -> Path: + project_dir = tmp_path_factory.mktemp("tmp-project") + print("\nTemporary project directory:") + print(str(project_dir)) + print("---") + yield project_dir + shutil.rmtree(str(project_dir)) + + +def _mock_multianimal_project(project_dir: Path): + video_dir = project_dir / "videos" + video_dir.mkdir(exist_ok=True) + + cfg_file = ProjectConfig( + multianimalproject=True, + Task="mock", + scorer="mock", + video_sets={SQS((video_dir / "vid.mp4").as_posix()): {"crop": "0, 640, 0, 480"}}, + project_path=project_dir.as_posix(), + individuals=["a", "b"], + uniquebodyparts=[], + multianimalbodyparts=["k1", "k2", "k3"], + bodyparts="MULTI!", + ) + cfg_file.to_yaml(project_dir / "config.yaml") + + +def _make_mock_loader( + project_path: Path, + project_task: str, + project_iteration: int, + model_folder: Path, + net_type: str, + pose_task: Task, + default_snapshot_index: int | str, + default_detector_snapshot_index: int | str, +) -> Mock: + loader = Mock() + loader.project_path = project_path + loader.model_folder = model_folder + loader.pose_task = pose_task + loader.shuffle = 0 + + loader.project_cfg = dict( + project_path=str(project_path), + Task=project_task, + date="Jan12", + TrainingFraction=[0.95], + snapshotindex=default_snapshot_index, + detector_snapshotindex=default_detector_snapshot_index, + iteration=project_iteration, + ) + loader.model_cfg = dict( + net_type=net_type, + metadata=dict( + project_path=str(project_path), + pose_config_path=str(loader.model_folder / "pytorch_config.yaml"), + ), + weight_init=None, + resume_training_from=None, + ) + if pose_task == Task.TOP_DOWN: + loader.model_cfg["detector"] = dict(resume_training_from=None) + + return loader + + +def test_wipe_paths_clears_resume_training_from(tmp_path: Path) -> None: + cfg = _minimal_pose_config(tmp_path) + export.wipe_paths_from_model_config(cfg) + assert cfg.resume_training_from is None + assert cfg.metadata.project_path is None + assert cfg.metadata.pose_config_path is None + + +def _get_export_model_data( + project_dir: Path, + num_snapshots: int, + task: Task, + project_iteration: int = 0, +): + _mock_multianimal_project(project_dir) + + model_dir = Path(project_dir) / f"iteration-{project_iteration}" / "fake-shuffle-0" + model_dir.mkdir(exist_ok=True, parents=True) + snapshots = [] + snapshot_data = [] + for i in range(num_snapshots): + snapshot = dict(model=dict(idx=i)) + snapshot_path = model_dir / f"snapshot-{i:03}.pt" + torch.save(snapshot, snapshot_path) + snapshots.append(Snapshot(best=False, epochs=i, path=snapshot_path)) + snapshot_data.append(snapshot) + + detector_snapshots = [] + detector_data = [] + if task == Task.TOP_DOWN: + for i in range(num_snapshots): + snapshot = dict(model=dict(idx=i)) + 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)) + + mock_loader = _make_mock_loader( + project_path=project_dir, + project_task="mock", + project_iteration=project_iteration, + model_folder=model_dir, + net_type="fake-net", + pose_task=task, + default_snapshot_index=-1, + default_detector_snapshot_index=-1, + ) + return mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data + + +@pytest.mark.parametrize( + "task, num_snapshots, idx, detector_idx", + [ + (Task.BOTTOM_UP, 10, 0, None), + (Task.BOTTOM_UP, 10, 5, None), + (Task.BOTTOM_UP, 10, -1, None), + (Task.TOP_DOWN, 10, 0, 0), + (Task.TOP_DOWN, 10, -1, 0), + (Task.TOP_DOWN, 10, -1, 5), + (Task.TOP_DOWN, 10, -1, -1), + ], +) +def test_export_model( + project_dir, + task: Task, + num_snapshots: int, + idx: int, + detector_idx: int | None, +): + test_data = _get_export_model_data(project_dir, num_snapshots, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + # export the model + export.export_model( + project_dir / "config.yaml", + snapshotindex=idx, + detector_snapshot_index=detector_idx, + ) + + # check that the correct snapshot was exported + snapshot = snapshots[idx] + detector = None + if task == Task.TOP_DOWN: + detector = detector_snapshots[detector_idx] + + dir_name = export.get_export_folder_name(mock_loader) + filename = export.get_export_filename(mock_loader, snapshot, detector) + expected_export = project_dir / "exported-models-pytorch" / dir_name / filename + assert expected_export.exists() + + # check that content of the exports are correct + exported_data = torch.load(expected_export, weights_only=True) + assert isinstance(exported_data, dict) + assert "config" in exported_data + assert exported_data["config"] == mock_loader.model_cfg + + assert "pose" in exported_data + assert exported_data["pose"] == snapshot_data[idx]["model"] + + if task == Task.TOP_DOWN: + assert "detector" in exported_data + assert exported_data["detector"] == detector_data[detector_idx]["model"] + + +@patch("deeplabcut.pose_estimation_pytorch.apis.export.wipe_paths_from_model_config") +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +def test_export_model_clear_paths(mock_wipe: Mock, project_dir, task: Task): + test_data = _get_export_model_data(project_dir, 1, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + export.export_model(project_dir / "config.yaml", wipe_paths=True) + + # check that wipe_paths_from_model_config was called + assert mock_wipe.call_count == 1 + + +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +@pytest.mark.parametrize("overwrite", [True, False]) +def test_export_overwrite(project_dir, task: Task, overwrite: bool): + test_data = _get_export_model_data(project_dir, 1, task) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + snapshot = snapshots[0] + detector = None if task == Task.BOTTOM_UP else detector_snapshots[0] + + def get_mock_loader(*args, **kwargs): + return mock_loader + + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + dir_name = export.get_export_folder_name(mock_loader) + filename = export.get_export_filename(mock_loader, snapshot, detector) + expected_export = project_dir / "exported-models-pytorch" / dir_name / filename + expected_export.parent.mkdir(exist_ok=False, parents=True) + + # add existing data + assert not expected_export.exists() + existing_data = dict() + torch.save(existing_data, expected_export) + + # export data + export.export_model(project_dir / "config.yaml", overwrite=overwrite) + + exported_data = torch.load(expected_export, weights_only=True) + + if overwrite: + assert existing_data != exported_data + else: + assert existing_data == exported_data + + +@pytest.mark.parametrize("task", [Task.BOTTOM_UP, Task.TOP_DOWN]) +@pytest.mark.parametrize("iteration", [5, 12]) +def test_export_change_iteration(project_dir, task: Task, iteration: int): + test_data = _get_export_model_data( + project_dir, + 1, + task, + project_iteration=0, + ) + mock_loader, snapshots, snapshot_data, detector_snapshots, detector_data = test_data + 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] + + def get_mock_loader(config, *args, **kwargs): + _loader = copy.deepcopy(mock_loader) + if isinstance(config, (dict, ProjectConfig)): + _loader.project_cfg = config + return _loader + + # patch the DLCLoader but also read_config + with patch( + "deeplabcut.pose_estimation_pytorch.apis.export.dlc3_data.DLCLoader", + get_mock_loader, + ): + # check no exports exist yet + 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() + + # export data + export.export_model(project_dir / "config.yaml", iteration=iteration) + + # check the export exists for the correct iteration + for loader, file_should_exist in [ + (mock_loader, False), + (loader_diff_iter, True), + ]: + dir_name = export.get_export_folder_name(loader) + filename = export.get_export_filename(loader, snapshot, detector) + expected = project_dir / "exported-models-pytorch" / dir_name / filename + expected_exists = expected.exists() + assert expected_exists == file_should_exist diff --git a/tests/pose_estimation_pytorch/apis/test_apis_training.py b/tests/pose_estimation_pytorch/apis/test_apis_training.py new file mode 100644 index 0000000000..e3f2fc2b11 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_training.py @@ -0,0 +1,57 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for the training API.""" + +from pathlib import Path +from unittest.mock import Mock, patch + +from deeplabcut.pose_estimation_pytorch.apis.training import train +from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config +from deeplabcut.pose_estimation_pytorch.config.pose import PoseConfig +from deeplabcut.pose_estimation_pytorch.task import Task + + +def _minimal_run_config(tmp_path: Path, *, resume_from: str | None = None) -> PoseConfig: + project_cfg = { + "multianimalproject": False, + "project_path": str(tmp_path), + "bodyparts": ["nose"], + "uniquebodyparts": [], + "individuals": ["mouse"], + } + cfg_path = tmp_path / "pytorch_config.yaml" + pose_config = make_pytorch_pose_config(project_cfg, str(cfg_path), net_type="resnet_50") + if resume_from is not None: + pose_config.resume_training_from = resume_from + return pose_config + + +@patch("deeplabcut.pose_estimation_pytorch.apis.training.build_transforms", return_value=Mock()) +@patch("deeplabcut.pose_estimation_pytorch.apis.training.PoseModel.build", return_value=Mock()) +@patch("deeplabcut.pose_estimation_pytorch.apis.training.build_training_runner", return_value=Mock()) +def test_train_uses_resume_training_from_config( + mock_build_runner: Mock, + mock_build_model: Mock, + mock_build_transforms: Mock, + tmp_path: Path, +) -> None: + run_config = _minimal_run_config(tmp_path, resume_from="/train/snapshot-010.pt") + + loader = Mock() + loader.model_folder = tmp_path + loader.model_cfg = run_config + train_dataset = Mock(__len__=Mock(return_value=1)) + valid_dataset = Mock(__len__=Mock(return_value=1)) + loader.create_dataset = Mock(side_effect=[train_dataset, valid_dataset]) + + train(loader=loader, run_config=run_config, task=Task.BOTTOM_UP, device="cpu", snapshot_path=None) + + assert mock_build_runner.call_args.kwargs["snapshot_path"] == "/train/snapshot-010.pt" diff --git a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py new file mode 100644 index 0000000000..e9ba4996d3 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py @@ -0,0 +1,74 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests method to create the tracking dataset in PyTorch.""" + +from pathlib import Path + +import torch + +import deeplabcut.pose_estimation_pytorch as dlc_torch +import deeplabcut.pose_estimation_pytorch.apis.tracking_dataset as tracking_dataset +import deeplabcut.pose_estimation_pytorch.models as models + + +class MockLoader(dlc_torch.Loader): + """Mock loader for data.""" + + def __init__(self, tmp_folder: Path, bodyparts: list[str] | None = None): + if bodyparts is None: + bodyparts = ["nose", "left_eye", "right_eye", "tail_base"] + self.bodyparts = bodyparts + + model_config_path = tmp_folder / "pytorch_config.yaml" + dlc_torch.config.make_pytorch_pose_config( + project_config=dlc_torch.config.make_basic_project_config( + dataset_path=str(tmp_folder), + bodyparts=self.bodyparts, + max_individuals=3, + ), + pose_config_path=tmp_folder / "pytorch_config.yaml", + net_type="resnet_50", + save=True, + ) + super().__init__( + str(tmp_folder), + str(tmp_folder / "labeled-data"), + model_config_path, + ) + + def load_data(self, mode: str = "train") -> dict[str, list[dict]]: + return { + "annotations": [], + "categories": [], + "images": [], + } + + def get_dataset_parameters(self) -> dlc_torch.PoseDatasetParameters: + return dlc_torch.PoseDatasetParameters( + bodyparts=self.bodyparts, + unique_bpts=[], + individuals=self.model_cfg["metadata"]["individuals"], + ) + + +def test_build_feature_extraction_runner(tmp_path_factory): + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + + loader = MockLoader(tmp_folder=tmp_folder) + model = models.PoseModel.build(loader.model_cfg["model"]) + snapshot_path = loader.model_folder / "snapshot.pt" + torch.save(dict(model=model.state_dict()), snapshot_path) + _ = tracking_dataset.build_feature_extraction_runner( + loader=loader, + snapshot_path=snapshot_path, + device="cpu", + batch_size=1, + ) diff --git a/tests/pose_estimation_pytorch/apis/test_tracklets.py b/tests/pose_estimation_pytorch/apis/test_tracklets.py new file mode 100644 index 0000000000..8e06b4a55e --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_tracklets.py @@ -0,0 +1,100 @@ +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.pose_estimation_pytorch.apis.tracklets import build_tracklets + + +@pytest.mark.parametrize( + "assemblies_data, inference_cfg, joints, scorer, num_frames, unique_bodyparts", + [ + ( + # assemblies_data + { + "single": { + 0: np.array([[1, 2, 0.9]]), + 1: np.array([[1, 3, 0.7]]), + 2: np.array([[0, 1, 0.9]]), + }, + 0: [ + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + ], + 1: [ + np.array([[9, 19, 0.9, -1], [29, 41, 0.8, -1]]), + np.array([[15, 21, 0.9, -1], [35, 45, 0.8, -1]]), + ], + 2: [ + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + ], + }, + # inference_cfg + {"max_age": 3, "min_hits": 1, "topktoretain": 1, "pcutoff": 0.5}, + # joints + ["nose", "ear"], + # scorer + "DLC", + # num_frames + 3, + # unique_bodyparts + ["led"], + ), + ( + # assemblies_data + { + 0: [ + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + ], + 1: [ + np.array([[9, 19, 0.9, -1], [29, 41, 0.8, -1]]), + np.array([[15, 21, 0.9, -1], [35, 45, 0.8, -1]]), + ], + 2: [ + np.array([[13, 23, 0.9, -1], [33, 43, 0.8, -1]]), + np.array([[10, 20, 0.9, -1], [30, 40, 0.8, -1]]), + ], + }, + # inference_cfg + {"max_age": 3, "min_hits": 1, "topktoretain": 1, "pcutoff": 0.5}, + # joints + ["nose", "ear"], + # scorer + "DLC", + # num_frames + 3, + # unique_bodyparts + None, + ), + ], +) +def test_build_tracklets( + assemblies_data: dict, + inference_cfg: dict, + joints: list, + scorer: str, + num_frames: int, + unique_bodyparts: list, +): + # Run the function + tracklets = build_tracklets( + assemblies_data=assemblies_data, + track_method="box", + inference_cfg=inference_cfg, + joints=joints, + scorer=scorer, + num_frames=num_frames, + unique_bodyparts=unique_bodyparts, + identity_only=False, + ) + + # # Assertions + assert "header" in tracklets + assert isinstance(tracklets["header"], pd.MultiIndex) + if unique_bodyparts: + assert "single" in tracklets + else: + assert "single" not in tracklets + + assert isinstance(tracklets, dict) diff --git a/tests/pose_estimation_pytorch/config/fixtures/multianimal_project_v0.yaml b/tests/pose_estimation_pytorch/config/fixtures/multianimal_project_v0.yaml new file mode 100644 index 0000000000..8191ad124e --- /dev/null +++ b/tests/pose_estimation_pytorch/config/fixtures/multianimal_project_v0.yaml @@ -0,0 +1,79 @@ +# Config schema version. Do not edit manually. +config_version: 0 +# Project definitions (do not edit) +Task: multi_mouse +scorer: test +date: Jun19 +multianimalproject: true +identity: false + +# Project path (change when moving around) +project_path: /test/fixtures/multianimal_project_v0 + +# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) +engine: pytorch + +# Annotation data set configuration (and individual video cropping parameters) +video_sets: + /test/fixtures/multianimal_project_v0/videos/m3v1.mp4: + crop: 0, 640, 0, 480 + /test/fixtures/multianimal_project_v0/videos/m3v2.mp4: + crop: 0, 640, 0, 480 +bodyparts: MULTI! +individuals: +- mouse1 +- mouse2 +uniquebodyparts: +- corner1 +- corner2 +multianimalbodyparts: +- nose +- tail + +# Fraction of video to start/stop when extracting frames for labeling/refinement +start: 0.0 +stop: 1.0 +numframes2pick: 5 + +# Plotting configuration +skeleton: +- - nose + - tail +skeleton_color: black +pcutoff: 0.6 +dotsize: 12 +alphavalue: 0.7 +colormap: rainbow + +# Training,Evaluation and Analysis configuration +TrainingFraction: +- 0.8 +iteration: 1 +default_net_type: resnet_50 +default_augmenter: albumentations +default_track_method: ellipse +snapshotindex: -1 +detector_snapshotindex: -1 +batch_size: 8 +detector_batch_size: 1 + +# Cropping Parameters (for analysis and outlier frame detection) +cropping: false +# if cropping is true for analysis, then set the values here: +x1: 0 +x2: 640 +y1: 277 +y2: 624 + +# Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +- 50 +- 50 +move2corner: true + +# Conversion tables to fine-tune SuperAnimal weights +SuperAnimalConversionTables: + +# These are very old parameters that are no longer used They are simply ignored. +resnet: +croppedtraining: diff --git a/tests/pose_estimation_pytorch/config/fixtures/single_animal_project_v0.yaml b/tests/pose_estimation_pytorch/config/fixtures/single_animal_project_v0.yaml new file mode 100644 index 0000000000..49533afe0c --- /dev/null +++ b/tests/pose_estimation_pytorch/config/fixtures/single_animal_project_v0.yaml @@ -0,0 +1,71 @@ +# Config schema version. Do not edit manually. +config_version: 0 +# Project definitions (do not edit) +Task: openfield +scorer: test +date: Oct30 +multianimalproject: false +identity: false + +# Project path (change when moving around) +project_path: /test/fixtures/single_animal_project_v0 + +# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) +engine: pytorch + +# Annotation data set configuration (and individual video cropping parameters) +video_sets: + /test/fixtures/single_animal_project_v0/videos/m1s1.mp4: + crop: 0, 640, 0, 480 + /test/fixtures/single_animal_project_v0/videos/m1s2.mp4: + crop: 0, 640, 0, 480 +bodyparts: +- snout +- leftear +- rightear +- tailbase + +# Fraction of video to start/stop when extracting frames for labeling/refinement +start: 0.0 +stop: 1.0 +numframes2pick: 20 + +# Plotting configuration +skeleton: [] +skeleton_color: black +pcutoff: 0.4 +dotsize: 8 +alphavalue: 0.7 +colormap: jet + +# Training,Evaluation and Analysis configuration +TrainingFraction: +- 0.95 +iteration: 0 +default_net_type: resnet_50 +default_augmenter: default +snapshotindex: -1 +detector_snapshotindex: -1 +batch_size: 4 +detector_batch_size: 1 + +# Cropping Parameters (for analysis and outlier frame detection) +cropping: false +# if cropping is true for analysis, then set the values here: +x1: 0 +x2: 640 +y1: 0 +y2: 480 + +# Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +- 50 +- 50 +move2corner: true + +# Conversion tables to fine-tune SuperAnimal weights +SuperAnimalConversionTables: + +# These are very old parameters that are no longer used They are simply ignored. +resnet: +croppedtraining: diff --git a/tests/pose_estimation_pytorch/config/test_config_utils.py b/tests/pose_estimation_pytorch/config/test_config_utils.py new file mode 100644 index 0000000000..4a28ef567a --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_config_utils.py @@ -0,0 +1,67 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Test util functions for config creation.""" + +import pytest + +import deeplabcut.pose_estimation_pytorch.config.utils as utils + + +@pytest.mark.parametrize( + "data", + [ + dict( + config={}, + num_bodyparts=None, + num_individuals=None, + backbone_output_channels=None, + output_config={}, + ), + dict( + config={ + "a": "num_bodyparts", + "b": ["num_bodyparts // 2", "num_bodyparts // 3"], + "c": "num_bodyparts x 2", + "d": "num_bodyparts + 2", + }, + num_bodyparts=10, + num_individuals=None, + backbone_output_channels=None, + output_config={ + "a": 10, + "b": [5, 3], + "c": 20, + "d": 12, + }, + ), + dict( + config={ + "a": [{"b": "num_individuals x 3"}], + "b": [[{"b": "num_bodyparts x 3"}]], + }, + num_bodyparts=10, + num_individuals=1, + backbone_output_channels=None, + output_config={ + "a": [{"b": 3}], + "b": [[{"b": 30}]], + }, + ), + ], +) +def test_replace_default_values_no_extras(data: dict): + output_config = utils.replace_default_values( + config=data["config"], + num_bodyparts=data["num_bodyparts"], + num_individuals=data["num_individuals"], + backbone_output_channels=data["backbone_output_channels"], + ) + assert output_config == data["output_config"] diff --git a/tests/pose_estimation_pytorch/config/test_ctd_conditions.py b/tests/pose_estimation_pytorch/config/test_ctd_conditions.py new file mode 100644 index 0000000000..725f055354 --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_ctd_conditions.py @@ -0,0 +1,374 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for typed CTD conditions config and evaluation loading.""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from deeplabcut.pose_estimation_pytorch import data as data_module +from deeplabcut.pose_estimation_pytorch.apis.ctd import load_conditions_for_evaluation +from deeplabcut.pose_estimation_pytorch.config.ctd_conditions import ( + ConditionsConfig, + ConditionsFileConfig, + ConditionsModelConfig, + ConditionsShuffleConfig, +) +from deeplabcut.pose_estimation_pytorch.config.inference import InferenceConfig +from deeplabcut.pose_estimation_pytorch.config.make_pose_config import _add_ctd_conditions +from deeplabcut.pose_estimation_pytorch.config.pose import PoseConfig +from deeplabcut.pose_estimation_pytorch.task import Task + + +def _ctd_model_conditions_config() -> ConditionsModelConfig: + return ConditionsModelConfig( + config_path=Path("/bu/pytorch_config.yaml"), + snapshot_path=Path("/bu/snapshot-best.pt"), + scorer="DLC_resnet50", + ) + + +def _ctd_project(tmp_path: Path) -> dict: + return { + "multianimalproject": True, + "project_path": str(tmp_path), + "bodyparts": "MULTI!", + "multianimalbodyparts": ["nose", "tail"], + "uniquebodyparts": [], + "individuals": ["mouse1", "mouse2"], + "identity": False, + } + + +def _ctd_loader(*, conditions) -> Mock: + loader = Mock() + loader.pose_task = Task.COND_TOP_DOWN + loader.model_cfg = {"inference": {"conditions": conditions}} + loader.image_root = Path("/images") + return loader + + +class _FakeDLCLoader(data_module.DLCLoader): + """Minimal DLCLoader stand-in so isinstance(..., DLCLoader) succeeds.""" + + def __init__(self, conditions): + self.pose_task = Task.COND_TOP_DOWN + self.model_cfg = {"inference": {"conditions": conditions}} + self.image_root = Path("/images") + self.project_root = Path("/project") + + +# --- ConditionsConfig.build --------------------------------------------------- + + +@pytest.mark.parametrize( + "raw, expected_type, check", + [ + pytest.param(None, type(None), lambda c: c is None, id="none"), + pytest.param( + "/path/to/preds.h5", + ConditionsFileConfig, + lambda c: c.filepath == Path("/path/to/preds.h5"), + id="path-str", + ), + pytest.param( + Path("/path/to/preds.json"), + ConditionsFileConfig, + lambda c: c.filepath == Path("/path/to/preds.json"), + id="path-obj", + ), + pytest.param( + {"filepath": "/preds.h5"}, + ConditionsFileConfig, + lambda c: c.filepath == Path("/preds.h5"), + id="filepath-dict", + ), + pytest.param( + {"shuffle": 3}, + ConditionsShuffleConfig, + lambda c: c.shuffle == 3, + id="shuffle-dict", + ), + pytest.param( + {"shuffle": 1, "snapshot_index": -1}, + ConditionsShuffleConfig, + lambda c: c.shuffle == 1 and c.snapshot_index == -1, + id="shuffle-snapshot-index", + ), + pytest.param( + { + "config_path": "/bu/pytorch_config.yaml", + "snapshot_path": "/bu/snapshot.pt", + }, + ConditionsModelConfig, + lambda c: c.config_path == Path("/bu/pytorch_config.yaml"), + id="model-dict", + ), + pytest.param( + {"source": "shuffle", "shuffle": 9}, + ConditionsShuffleConfig, + lambda c: c.shuffle == 9, + id="explicit-source-shuffle", + ), + ], +) +def test_conditions_config_build(raw, expected_type, check): + built = ConditionsConfig.build(raw) + assert isinstance(built, expected_type) + assert check(built) + + +def test_conditions_config_build_passthrough(): + ctd_conditions = _ctd_model_conditions_config() + assert ConditionsConfig.build(ctd_conditions) is ctd_conditions + + +def test_conditions_config_build_rejects_ambiguous_dict(): + with pytest.raises(ValueError, match="Cannot determine conditions source"): + ConditionsConfig.build({"unexpected": 1}) + + +def test_conditions_config_build_rejects_unsupported_type(): + with pytest.raises(TypeError, match="Cannot build"): + ConditionsConfig.build(42) # type: ignore[arg-type] + + +# --- resolve_from_conditions -------------------------------------------------- + + +def test_resolve_from_conditions_model_identity(): + ctd_conditions = _ctd_model_conditions_config() + assert ConditionsModelConfig.resolve_from_conditions(ctd_conditions) is ctd_conditions + + +def test_resolve_from_conditions_rejects_file_config(): + with pytest.raises(ValueError, match="evaluation only"): + ConditionsModelConfig.resolve_from_conditions(ConditionsFileConfig(filepath=Path("/preds.h5"))) + + +def test_resolve_from_conditions_rejects_filepath_dict(): + with pytest.raises(ValueError, match="evaluation only"): + ConditionsModelConfig.resolve_from_conditions({"filepath": "/preds.h5"}) + + +@pytest.mark.parametrize( + "conditions", + [ + pytest.param({"shuffle": 1}, id="dict"), + pytest.param(ConditionsShuffleConfig(shuffle=1), id="typed"), + ], +) +def test_resolve_from_conditions_shuffle_requires_config(conditions): + with pytest.raises(ValueError, match="no project config"): + ConditionsModelConfig.resolve_from_conditions(conditions) + + +@pytest.mark.parametrize( + "conditions", + [ + pytest.param( + ConditionsShuffleConfig(shuffle=7, snapshot_index=-1), + id="typed", + ), + pytest.param( + {"shuffle": 7, "snapshot_index": -1}, + id="dict", + ), + ], +) +def test_resolve_from_conditions_shuffle_with_config(conditions, monkeypatch): + """Shuffle forms are forwarded to from_shuffle with the project config injected.""" + mock_from_shuffle = Mock(return_value=_ctd_model_conditions_config()) + monkeypatch.setattr( + ConditionsModelConfig, + "from_shuffle", + classmethod(lambda cls, **kwargs: mock_from_shuffle(**kwargs)), + ) + + ConditionsModelConfig.resolve_from_conditions( + conditions, + config="/project/config.yaml", + ) + + mock_from_shuffle.assert_called_once_with( + config=Path("/project/config.yaml"), + shuffle=7, + trainset_index=0, + modelprefix="", + snapshot=None, + snapshot_index=-1, + ) + + +def test_resolve_from_conditions_uses_embedded_shuffle_config(monkeypatch): + mock_from_shuffle = Mock(return_value=_ctd_model_conditions_config()) + monkeypatch.setattr( + ConditionsModelConfig, + "from_shuffle", + classmethod(lambda cls, **kwargs: mock_from_shuffle(**kwargs)), + ) + + ConditionsModelConfig.resolve_from_conditions( + ConditionsShuffleConfig(shuffle=3, config=Path("/embedded/config.yaml")), + ) + + mock_from_shuffle.assert_called_once_with( + config=Path("/embedded/config.yaml"), + shuffle=3, + trainset_index=0, + modelprefix="", + snapshot=None, + snapshot_index=None, + ) + + +# --- InferenceConfig + _add_ctd_conditions / PoseConfig ------------------------ + + +def test_inference_config_accepts_path_string(): + """Regression: bare path strings must validate (PoseConfig / YAML file form).""" + cfg = InferenceConfig(conditions="/path/to/bu_predictions.h5") + assert isinstance(cfg.conditions, ConditionsFileConfig) + assert cfg.conditions.filepath == Path("/path/to/bu_predictions.h5") + + +@pytest.mark.parametrize( + "ctd_conditions, expected", + [ + pytest.param(5, {"shuffle": 5}, id="int"), + pytest.param((1, -1), {"shuffle": 1, "snapshot_index": -1}, id="tuple-index"), + pytest.param( + (2, "snapshot-best-150.pt"), + {"shuffle": 2, "snapshot": "snapshot-best-150.pt"}, + id="tuple-name", + ), + ], +) +def test_add_ctd_conditions_shuffle_forms(ctd_conditions, expected): + model_cfg: dict = {"inference": {}} + _add_ctd_conditions(model_cfg, ctd_conditions) + assert model_cfg["inference"]["conditions"] == expected + built = ConditionsConfig.build(model_cfg["inference"]["conditions"]) + assert isinstance(built, ConditionsShuffleConfig) + + +def test_add_ctd_conditions_file_path(tmp_path: Path): + preds = tmp_path / "bu_predictions.h5" + preds.write_bytes(b"") + + model_cfg: dict = {"inference": {}} + _add_ctd_conditions(model_cfg, preds) + assert model_cfg["inference"]["conditions"] == str(preds.resolve()) + + built = ConditionsConfig.build(model_cfg["inference"]["conditions"]) + assert isinstance(built, ConditionsFileConfig) + assert built.filepath == preds.resolve() + + +def test_pose_config_build_ctd_with_shuffle(tmp_path: Path): + pose_cfg = PoseConfig.build( + _ctd_project(tmp_path), + tmp_path / "pytorch_config.yaml", + top_down=False, + net_type="ctd_coam_w32", + ctd_conditions=(1, -1), + ) + assert isinstance(pose_cfg.inference.conditions, ConditionsShuffleConfig) + assert pose_cfg.inference.conditions.shuffle == 1 + assert pose_cfg.inference.conditions.snapshot_index == -1 + + +def test_pose_config_build_ctd_with_file(tmp_path: Path): + preds = tmp_path / "conditions.h5" + preds.write_bytes(b"") + pose_cfg = PoseConfig.build( + _ctd_project(tmp_path), + tmp_path / "pytorch_config.yaml", + top_down=False, + net_type="ctd_coam_w32", + ctd_conditions=preds, + ) + assert isinstance(pose_cfg.inference.conditions, ConditionsFileConfig) + assert pose_cfg.inference.conditions.filepath == preds.resolve() + + +# --- load_conditions_for_evaluation ------------------------------------------- + + +def test_load_conditions_for_evaluation_from_file(): + loader = _ctd_loader(conditions="/preds.h5") # YAML-like raw path string + + with patch("deeplabcut.pose_estimation_pytorch.apis.ctd.CondFromFile") as mock_cond: + load_conditions_for_evaluation(loader, ["img.png"]) + + mock_cond.assert_called_once_with(filepath=Path("/preds.h5")) + mock_cond.return_value.load_conditions.assert_called_once_with(["img.png"], path_prefix=loader.image_root) + + +def test_load_conditions_for_evaluation_from_shuffle(): + loader = _ctd_loader( + conditions={ + "shuffle": 4, + "config": "/project/config.yaml", + "snapshot": "snapshot-100.pt", + } + ) + + with patch("deeplabcut.pose_estimation_pytorch.apis.ctd.CondFromFile") as mock_cond: + load_conditions_for_evaluation(loader, ["img.png"]) + + mock_cond.assert_called_once_with( + config=Path("/project/config.yaml"), + shuffle=4, + trainset_index=0, + modelprefix="", + snapshot="snapshot-100.pt", + snapshot_index=None, + ) + + +def test_load_conditions_for_evaluation_injects_dlcloader_project_config(): + loader = _FakeDLCLoader(conditions={"shuffle": 4}) + + with patch("deeplabcut.pose_estimation_pytorch.apis.ctd.CondFromFile") as mock_cond: + load_conditions_for_evaluation(loader, ["img.png"]) + + mock_cond.assert_called_once_with( + config=Path("/project/config.yaml"), + shuffle=4, + trainset_index=0, + modelprefix="", + snapshot=None, + snapshot_index=None, + ) + + +def test_load_conditions_for_evaluation_rejects_model(): + with pytest.raises(ValueError, match="Evaluation accepts file paths or shuffle refs"): + load_conditions_for_evaluation( + _ctd_loader(conditions=_ctd_model_conditions_config()), + ["img.png"], + ) + + +def test_load_conditions_for_evaluation_rejects_none(): + with pytest.raises(ValueError, match="Got None"): + load_conditions_for_evaluation(_ctd_loader(conditions=None), ["img.png"]) + + +def test_load_conditions_for_evaluation_rejects_non_ctd(): + loader = _ctd_loader(conditions={"shuffle": 1}) + loader.pose_task = Task.BOTTOM_UP + with pytest.raises(ValueError, match="only be loaded for CTD"): + load_conditions_for_evaluation(loader, ["img.png"]) diff --git a/tests/pose_estimation_pytorch/config/test_make_pose_config.py b/tests/pose_estimation_pytorch/config/test_make_pose_config.py new file mode 100644 index 0000000000..a8fbc7b6ff --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_make_pose_config.py @@ -0,0 +1,484 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the pre-processors.""" + +import pytest + +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.core.config import pretty_print +from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( + make_basic_project_config, + make_pytorch_pose_config, +) +from deeplabcut.pose_estimation_pytorch.config.utils import ( + update_config, + update_config_by_dotpath, +) + + +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@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( + project_path="my/little/project", + multianimal=False, + identity=False, + individuals=[], + bodyparts=bodyparts, + unique_bodyparts=[], + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + # check that the bodypart head has locref and heatmaps and the correct output shapes + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + outputs = [("heatmap_config", len(bodyparts))] + if bodypart_head["predictor"]["location_refinement"]: + outputs += [("locref_config", 2 * len(bodyparts))] + + for name, output_channels in outputs: + head = bodypart_head[name] + if "final_conv" in head: + actual_output_channels = head["final_conv"]["out_channels"] + else: + actual_output_channels = head["channels"][-1] + assert name in bodypart_head + assert actual_output_channels == output_channels + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@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"]) +def test_backbone_plus_paf_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + # Single animal projects can't have unique bodyparts + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + 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 + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + # check PAF head + assert bodypart_head["type"] == "DLCRNetHead" + assert bodypart_head["predictor"]["type"] == "PartAffinityFieldPredictor" + + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ("locref_config", len(bodyparts) * 2), + ("paf_config", num_limbs), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize( + "detector", + [ + (None, "SSDLite"), + ("ssdlite", "SSDLite"), + ("fasterrcnn_mobilenet_v3_large_fpn", "FasterRCNN"), + ("fasterrcnn_resnet50_fpn_v2", "FasterRCNN"), + ], +) +@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"]) +def test_top_down_config( + detector: tuple[str, str], + individuals: list[str], + bodyparts: list[str], + net_type: str, +): + # Single animal projects can't have unique bodyparts + detector_type, expected_detector_type = detector + project_config = _make_project_config( + project_path="my/little/project", + multianimal=True, + identity=False, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=[], + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + top_down=True, + detector_type=detector_type, + ) + pretty_print(pytorch_pose_config) + + # check no collate function + collate = pytorch_pose_config["data"]["train"].get("collate") + print(f"Collate: {collate}") + assert not collate + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + + # check detector is there + assert "detector" in pytorch_pose_config.keys() + assert pytorch_pose_config["detector"]["model"]["type"] == expected_detector_type + + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["final_conv"]["out_channels"] == output_channels + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@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", ["dekr_w18", "dekr_w32", "dekr_w48"]) +def test_make_dekr_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + for name, output_channels in [ + ("heatmap_config", len(bodyparts) + 1), + ("offset_config", len(bodyparts)), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize("multianimal", [True]) +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose", "ears"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["dlcrnet_stride16_ms5", "dlcrnet_stride32_ms5"]) +def test_make_dlcrnet_config( + multianimal: bool, + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=multianimal, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + 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))] + num_limbs = len(paf_graph) + + # check heads are there + assert "bodypart" in pytorch_pose_config["model"]["heads"].keys() + bodypart_head = pytorch_pose_config["model"]["heads"]["bodypart"] + for name, output_channels in [ + ("heatmap_config", len(bodyparts)), + ("locref_config", 2 * len(bodyparts)), + ("paf_config", 2 * num_limbs), + ]: + print(name, bodypart_head[name]["channels"]) + assert name in bodypart_head + assert bodypart_head[name]["channels"][-1] == output_channels + + if len(unique_bodyparts) > 0: + assert "unique_bodypart" in pytorch_pose_config["model"]["heads"].keys() + unique_bodypart_head = pytorch_pose_config["model"]["heads"]["unique_bodypart"] + for name, output_channels in [ + ("heatmap_config", len(unique_bodyparts)), + ("locref_config", 2 * len(unique_bodyparts)), + ]: + assert name in unique_bodypart_head + assert unique_bodypart_head[name]["channels"][-1] == output_channels + assert unique_bodypart_head["target_generator"]["heatmap_mode"] == "KEYPOINT" + + if identity: + assert "identity" in pytorch_pose_config["model"]["heads"].keys() + id_head = pytorch_pose_config["model"]["heads"]["identity"] + assert "heatmap_config" in id_head + assert id_head["heatmap_config"]["channels"][-1] == len(individuals) + assert "locref_config" not in id_head + assert id_head["target_generator"]["heatmap_mode"] == "INDIVIDUAL" + + +@pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) +@pytest.mark.parametrize("bodyparts", [["nose", "eyes"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("identity", [False, True]) +@pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) +@pytest.mark.parametrize("net_type", ["animaltokenpose_base"]) +def test_make_tokenpose_config( + individuals: list[str], + bodyparts: list[str], + identity: bool, + unique_bodyparts: list[str], + net_type: str, +): + project_config = _make_project_config( + project_path="my/little/project", + multianimal=True, + identity=identity, + individuals=individuals, + bodyparts=bodyparts, + unique_bodyparts=unique_bodyparts, + ) + + if identity or len(unique_bodyparts) > 0: + with pytest.raises(ValueError) as _: + # Not yet implemented! + _ = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + else: + pytorch_pose_config = make_pytorch_pose_config( + project_config, + "pytorch_config.yaml", + net_type=net_type, + ) + pretty_print(pytorch_pose_config) + + # check no collate function + collate = pytorch_pose_config["data"]["train"].get("collate") + print(f"Collate: {collate}") + assert not collate + + # check detector is there + assert "detector" in pytorch_pose_config + assert "data" in pytorch_pose_config["detector"] + + +@pytest.mark.parametrize( + "data", + [ + { + "config": {"a": 0, "b": 0}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": {"i0": [1, 2, 3]}}, + "expected_result": {"a": 0, "b": {"i0": [1, 2, 3], "i1": 2}}, + }, + { + "config": {"detector": {"batch_size": 1, "epochs": 10, "save_epochs": 5}}, + "updates": { + "batch_size": 1, + "detector": {"batch_size": 8, "save_epochs": 1}, + }, + "expected_result": { + "batch_size": 1, + "detector": {"batch_size": 8, "epochs": 10, "save_epochs": 1}, + }, + }, + ], +) +def test_update_config(data: dict): + result = update_config(config=data["config"], updates=data["updates"]) + print("\nResult") + pretty_print(result) + assert result == data["expected_result"] + + +@pytest.mark.parametrize( + "data", + [ + { + "config": {"a": 0, "b": 0}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b": 1}, + "expected_result": {"a": 0, "b": 1}, + }, + { + "config": {"a": 0, "b": {"i0": 1, "i1": 2}}, + "updates": {"b.i0": [1, 2, 3]}, + "expected_result": {"a": 0, "b": {"i0": [1, 2, 3], "i1": 2}}, + }, + { + "config": {"detector": {"batch_size": 1, "epochs": 10, "save_epochs": 5}}, + "updates": { + "batch_size": 1, + "detector.batch_size": 8, + "detector.save_epochs": 1, + }, + "expected_result": { + "batch_size": 1, + "detector": {"batch_size": 8, "epochs": 10, "save_epochs": 1}, + }, + }, + ], +) +def test_update_config_by_dotpath(data: dict): + result = update_config_by_dotpath(config=data["config"], updates=data["updates"]) + print("\nResult") + pretty_print(result) + assert result == data["expected_result"] + + +def _make_project_config( + project_path: str, + multianimal: bool, + identity: bool, + individuals: list[str], + bodyparts: list[str], + unique_bodyparts: list[str], +) -> dict: + project_config = { + "project_path": project_path, + "multianimalproject": multianimal, + "identity": identity, + "uniquebodyparts": unique_bodyparts, + } + + if multianimal: + project_config["multianimalbodyparts"] = bodyparts + project_config["bodyparts"] = "MULTI!" + project_config["individuals"] = individuals + else: + project_config["bodyparts"] = bodyparts + + return project_config + + +@pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) +@pytest.mark.parametrize("max_idv", [1, 12, 20]) +@pytest.mark.parametrize("multi", [True, False]) +def test_make_basic_project_config(bodyparts: list[str], max_idv: int, multi: bool): + if not multi and max_idv > 1: + return + + project_config = make_basic_project_config( + dataset_path="path/dataset", + bodyparts=bodyparts, + max_individuals=max_idv, + multi_animal=multi, + ) + + bpts = af.get_bodyparts(project_config) + assert bodyparts == bpts + + individuals = project_config["individuals"] + assert len(individuals) == max_idv + assert len(set(individuals)) == max_idv diff --git a/tests/pose_estimation_pytorch/config/test_pose_config_creation.py b/tests/pose_estimation_pytorch/config/test_pose_config_creation.py new file mode 100644 index 0000000000..80d7589957 --- /dev/null +++ b/tests/pose_estimation_pytorch/config/test_pose_config_creation.py @@ -0,0 +1,217 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Tests for pose config creation via make_pytorch_pose_config.""" + +from __future__ import annotations + +import copy +from pathlib import Path +from typing import Any, NamedTuple + +import pytest + +from deeplabcut.core.config import read_config_as_dict +from deeplabcut.core.weight_init import WeightInitialization + +try: + from deeplabcut.pose_estimation_pytorch.config import PoseConfig + + build_pose_config = PoseConfig.build +except ImportError: + from deeplabcut.pose_estimation_pytorch.config.make_pose_config import make_pytorch_pose_config as build_pose_config + + +FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" + + +def _as_dict(cfg: Any) -> dict: + if isinstance(cfg, dict): + return cfg + return cfg.to_dict(normalize=True) + + +SINGLE_ANIMAL_PROJECT = read_config_as_dict(FIXTURES_DIR / "single_animal_project_v0.yaml") +MULTIANIMAL_PROJECT = read_config_as_dict(FIXTURES_DIR / "multianimal_project_v0.yaml") +MULTIANIMAL_TD_PROJECT = copy.deepcopy(MULTIANIMAL_PROJECT) +MULTIANIMAL_TD_PROJECT["uniquebodyparts"] = [] + + +class PoseConfigCase(NamedTuple): + id: str + project: dict + net_type: str + build_kwargs: dict[str, Any] + method: str + bodyparts: list[str] + individuals: list[str] + unique_bodyparts: list[str] + backbone_type: str | None + detector_type: str | None = None + head_type: str | None = None + + +POSE_CONFIG_CASES = [ + PoseConfigCase( + "single_resnet_50_bu", + SINGLE_ANIMAL_PROJECT, + "resnet_50", + {}, + "bu", + ["snout", "leftear", "rightear", "tailbase"], + ["individual_1"], + [], + "ResNet", + head_type="HeatmapHead", + ), + PoseConfigCase( + "single_hrnet_w32_bu", + SINGLE_ANIMAL_PROJECT, + "hrnet_w32", + {}, + "bu", + ["snout", "leftear", "rightear", "tailbase"], + ["individual_1"], + [], + "HRNet", + head_type="HeatmapHead", + ), + PoseConfigCase( + "multi_resnet_50_bu", + MULTIANIMAL_PROJECT, + "resnet_50", + {}, + "bu", + ["nose", "tail"], + ["mouse1", "mouse2"], + ["corner1", "corner2"], + "ResNet", + head_type="DLCRNetHead", + ), + PoseConfigCase( + "multi_dlcrnet_stride16_ms5_bu", + MULTIANIMAL_PROJECT, + "dlcrnet_stride16_ms5", + {}, + "bu", + ["nose", "tail"], + ["mouse1", "mouse2"], + ["corner1", "corner2"], + "DLCRNet", + head_type="DLCRNetHead", + ), + PoseConfigCase( + "multi_dekr_w18_bu", + MULTIANIMAL_PROJECT, + "dekr_w18", + {}, + "bu", + ["nose", "tail"], + ["mouse1", "mouse2"], + ["corner1", "corner2"], + "HRNet", + head_type="DEKRHead", + ), + PoseConfigCase( + "multi_resnet_50_td_ssdlite", + MULTIANIMAL_TD_PROJECT, + "resnet_50", + {"top_down": True, "detector_type": "ssdlite"}, + "td", + ["nose", "tail"], + ["mouse1", "mouse2"], + [], + "ResNet", + detector_type="SSDLite", + head_type="HeatmapHead", + ), +] + + +@pytest.mark.parametrize("case", POSE_CONFIG_CASES, ids=lambda c: c.id) +def test_pose_config_creation(case: PoseConfigCase, tmp_path: Path) -> None: + build_kwargs = dict(case.build_kwargs) + cfg = _as_dict( + build_pose_config( + case.project, + tmp_path / "pytorch_config.yaml", + net_type=case.net_type, + save=False, + top_down=build_kwargs.pop("top_down", False), + **build_kwargs, + ) + ) + meta = cfg["metadata"] + + assert str(cfg["net_type"]) == case.net_type + assert str(cfg["method"]).lower() == case.method + assert meta["bodyparts"] == case.bodyparts + if case.project["multianimalproject"]: + assert meta["individuals"] == case.individuals + else: + assert len(meta["individuals"]) == 1 + assert (meta.get("unique_bodyparts") or []) == case.unique_bodyparts + + if case.backbone_type is not None: + assert cfg["model"]["backbone"]["type"] == case.backbone_type + + if case.head_type is not None: + assert cfg["model"]["heads"]["bodypart"]["type"] == case.head_type + + if case.detector_type is not None: + detector = cfg["detector"] + assert detector is not None + assert detector["model"]["type"] == case.detector_type + assert detector["device"] == "auto" + assert detector["model"].get("box_score_thresh") is None + assert cfg["data"]["train"].get("top_down_crop") is not None + else: + assert cfg.get("detector") is None + + +def test_pose_config_save_writes_yaml(tmp_path: Path) -> None: + config_path = tmp_path / "pytorch_config.yaml" + cfg = build_pose_config( + SINGLE_ANIMAL_PROJECT, + config_path, + net_type="resnet_50", + top_down=False, + save=True, + ) + assert config_path.is_file() + saved = read_config_as_dict(config_path) + cfg_dict = _as_dict(cfg) + assert saved["net_type"] == cfg_dict["net_type"] + assert saved["metadata"]["bodyparts"] == cfg_dict["metadata"]["bodyparts"] + + +@pytest.mark.parametrize("project", [SINGLE_ANIMAL_PROJECT, MULTIANIMAL_PROJECT], ids=["single", "multi"]) +def test_default_net_type_used_when_net_type_is_none(project: dict) -> None: + cfg = build_pose_config(project, "pytorch_config.yaml", net_type=None, top_down=False) + assert str(_as_dict(cfg)["net_type"]) == "resnet_50" + + +def test_weight_init_is_written_to_train_settings() -> None: + project_config = { + "project_path": "/test/project", + "multianimalproject": False, + "identity": False, + "bodyparts": ["nose", "ear"], + } + snapshot_path = Path("/tmp/snapshot-010.pt") + weight_init = WeightInitialization(snapshot_path=snapshot_path) + + cfg = build_pose_config( + project_config, + "pytorch_config.yaml", + net_type="resnet_50", + weight_init=weight_init, + top_down=False, + ) + + saved = _as_dict(cfg)["train_settings"]["weight_init"] + assert saved["snapshot_path"] == str(snapshot_path) diff --git a/tests/pose_estimation_pytorch/data/test_data_ctd.py b/tests/pose_estimation_pytorch/data/test_data_ctd.py new file mode 100644 index 0000000000..37bd834d03 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_data_ctd.py @@ -0,0 +1,184 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import json +import platform +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromFile + +CONDITIONS = [ + np.zeros((4, 3, 3)).tolist(), + np.ones((4, 3, 3)).tolist(), + 2 * np.ones((4, 3, 3)).tolist(), + 3 * np.ones((4, 3, 3)).tolist(), +] + + +@pytest.mark.parametrize("path_prefix", ["/a/b"]) +@pytest.mark.parametrize( + "data", + [ + [("/a/b/c/d.png", "/a/b/c/d.png", CONDITIONS[1])], + [("/a/b/c/d.png", "c/d.png", CONDITIONS[1])], + [ + ("/a/b/c.png", "c.png", CONDITIONS[1]), + ("/a/b/c/d.png", "c/d.png", CONDITIONS[2]), + ("/a/b/c/e.png", "/a/b/c/e.png", CONDITIONS[3]), + ], + ], +) +def test_ctd_load_json_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + data: tuple[list[str], list[str], list], +) -> None: + print("Starting test") + # convert the image paths to Windows format + if platform.system() == "Windows": + print("Converting to windows filesystem") + + print("Path Prefix:", path_prefix) + if isinstance(path_prefix, Path): + print(f" As string: {str(path_prefix)}") + path_prefix = Path(_to_windows_path(str(path_prefix))) + else: + 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] + print(f"Images: {[d[0] for d in data]}") + print(f"Condition keys: {[d[1] for d in data]}") + print("---") + + images = [img for img, _, _ in data] + conditions = {key: cond for _, key, cond in data} + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + conditions_filepath = tmp_folder / "conditions.json" + with open(conditions_filepath, "w") as f: + json.dump(conditions, f) + + conditions = CondFromFile.load_conditions_json( + conditions_filepath, + images, + path_prefix=path_prefix, + ) + for img_path, _, condition in data: + assert img_path in conditions + np.testing.assert_allclose(condition, conditions[img_path]) + + +@pytest.mark.parametrize("path_prefix", ["/p"]) +@pytest.mark.parametrize("num_conditions", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize("num_bodyparts", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize( + "data", + [ + [("/p/data/video0/img0.png", ("data", "video0", "img0.png"))], + [("/p/data/video0/img0.png", "data/video0/img0.png")], + [ + ("/p/b/c/d0.png", ("b", "c", "d0.png")), + ("/p/b/c/d1.png", ("b", "c", "d1.png")), + ("/p/b/c/d2.png", ("b", "c", "d2.png")), + ], + [ + ("/p/b/c/d0.png", "b/c/d0.png"), + ("/p/b/c/d1.png", "b/c/d1.png"), + ("/p/b/c/d2.png", "b/c/d2.png"), + ], + ], +) +def test_ctd_load_hdf_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + num_conditions: int, + num_bodyparts: int, + data: tuple[list[str], list[str]], +) -> None: + print("\nStarting test") + + # convert the image paths to Windows format + if platform.system() == "Windows": + print("Converting to windows filesystem") + + print("Path Prefix:", path_prefix) + if isinstance(path_prefix, Path): + print(f" As string: {str(path_prefix)}") + path_prefix = Path(_to_windows_path(str(path_prefix))) + else: + path_prefix = _to_windows_path(path_prefix) + print(f" Converted {path_prefix}") + + data = [(_to_windows_path(img), idx) for img, idx in data] + print(f"Images: {[d[0] for d in data]}") + print("---") + + num_images = len(data) + images = [img for img, _ in data] + index = [idx for _, idx in data] + if isinstance(index[0], tuple): + index = pd.MultiIndex.from_tuples(index) + + # generate random pose data + size = (num_images, num_conditions, num_bodyparts, 3) + rng = np.random.default_rng(0) + pose = rng.integers(low=0, high=1024, size=size).astype(float) + pose[:, :, :, 2] = rng.random(size=(num_images, num_conditions, num_bodyparts)) + + # set some missing data + is_nans = rng.random(size=size) > 0.8 + pose[is_nans] = np.nan + + # create what the output data will look like + keypoint_mask = np.any(is_nans, axis=3) + output_pose = pose.copy() + output_pose[keypoint_mask] = 0.0 + 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, strict=False) + ] + + # generate columns for the dataframe + columns = pd.MultiIndex.from_product( + [ + ["scorer"], + [f"idv{i}" for i in range(num_conditions)], + [f"bpt{i}" for i in range(num_bodyparts)], + ["x", "y", "likelihood"], + ], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + df = pd.DataFrame(data=pose.reshape(num_images, -1), index=index, columns=columns) + + print(df.head()) + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + 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) + for idx, (img_path, _img_index) in enumerate(data): + assert img_path in conditions + np.testing.assert_allclose(output_pose[idx], conditions[img_path]) + + +def _to_windows_path(s: str) -> str: + # Convert absolute paths to paths on C: + if s.startswith("/"): + return str(Path("C:\\", *s[1:].split("/"))) + + return s diff --git a/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py b/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py new file mode 100644 index 0000000000..76fe04ca61 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_dlc_dataloader.py @@ -0,0 +1,68 @@ +from types import SimpleNamespace + +import numpy as np +import pandas as pd + +import deeplabcut.pose_estimation_pytorch.data.dlcloader as dlcloader_mod +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader + + +def test_to_coco_ignores_likelihood_columns(monkeypatch, tmp_path): + fake_shape = (3, 480, 640) + monkeypatch.setattr( + dlcloader_mod, + "read_image_shape_fast", + lambda _: fake_shape, + ) + + scorer = "testscorer" + bodyparts = ["nose", "tail"] + + index = pd.MultiIndex.from_tuples( + [("labeled-data", "video1", "img0001.png")], + names=["set", "video", "image"], + ) + + # Baseline dataframe: x/y only + columns_xy = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + df_xy = pd.DataFrame( + [[10.0, 20.0, 30.0, 40.0]], + index=index, + columns=columns_xy, + ) + + # Same data, but with likelihood columns added + columns_xyl = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "bodyparts", "coords"], + ) + df_xyl = pd.DataFrame( + [[10.0, 20.0, 0.9, 30.0, 40.0, 0.8]], + index=index, + columns=columns_xyl, + ) + + # to_coco only needs these attributes from parameters + params = SimpleNamespace( + bodyparts=bodyparts, + unique_bpts=[], + individuals=["animal"], + ) + + baseline = DLCLoader.to_coco(tmp_path, df_xy, params) + got = DLCLoader.to_coco(tmp_path, df_xyl, params) + + assert len(got["images"]) == len(baseline["images"]) == 1 + assert len(got["annotations"]) == len(baseline["annotations"]) == 1 + + got_ann = got["annotations"][0] + expected_ann = baseline["annotations"][0] + + assert got_ann["image_id"] == expected_ann["image_id"] + assert got_ann["category_id"] == expected_ann["category_id"] + assert got_ann["num_keypoints"] == expected_ann["num_keypoints"] == 2 + assert np.array_equal(got_ann["keypoints"], expected_ann["keypoints"]) + assert np.allclose(got_ann["bbox"], expected_ann["bbox"]) diff --git a/tests/pose_estimation_pytorch/data/test_postprocessor.py b/tests/pose_estimation_pytorch/data/test_postprocessor.py new file mode 100644 index 0000000000..354f63348c --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_postprocessor.py @@ -0,0 +1,423 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the pre-processors.""" + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data.postprocessor import ( + PredictKeypointIdentities, + PrepareBackboneFeatures, + RemoveLowConfidenceBoxes, + RescaleAndOffset, + TrimOutputs, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "predictions": [[[0, 0, 0.95], [20, 30, 0.5]]], + "offsets": [(0, 0)], + "scales": [(1, 1)], + "rescaled": [[[0, 0, 0.95], [20, 30, 0.5]]], + }, + { + "predictions": [ + [[0, 0, 0.12], [1000, 0, 0.5]], # individual 1 + [[18, 2, 0.24], [0, 1000, 0.6]], # individual 2 + ], + "offsets": [(0, 0), (0, 0)], + "scales": [(1, 1), (0.5, 1.0)], + "rescaled": [ + [[0, 0, 0.12], [1000, 0, 0.5]], # individual 1 + [[9, 2, 0.24], [0, 1000, 0.6]], # individual 2 + ], + }, + { + "predictions": [ + [[0, 0, 0.95], [20, 30, 0.5]], # individual 1 + [[110, 5, 0.95], [60, 1200, 0.5]], # individual 2 + ], + "offsets": [(12, 5), (27, 10)], + "scales": [(0.5, 0.5), (0.2, 0.2)], + "rescaled": [ + [[12, 5, 0.95], [22, 20, 0.5]], # individual 1 + [[49, 11, 0.95], [39, 250, 0.5]], # individual 2 + ], + }, + ], +) +def test_rescale_topdown(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bodyparts"], + mode=RescaleAndOffset.Mode.KEYPOINT_TD, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bodyparts": np.array(data["predictions"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bodyparts"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bodyparts"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[0, 0, 0, 0], [1, 1, 1, 1]], + "bbox_scores": [0, 0], + "max_individuals": {"bboxes": 1, "bbox_scores": 1}, + }, + { + "bboxes": [[0, 0, 0, 0], [1, 1, 1, 1]], + "bbox_scores": [0, 0], + "max_individuals": {"bboxes": 2, "bbox_scores": 2}, + }, + ], +) +def test_trim_outputs(data): + """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"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bboxes"].tolist()) + print(predictions["bbox_scores"].tolist()) + assert len(predictions["bboxes"]) == data["max_individuals"]["bboxes"] + assert len(predictions["bbox_scores"]) == data["max_individuals"]["bbox_scores"] + + +@pytest.mark.parametrize( + "data", + [ + { + "predictions": [[[0, 0, 0.95], [20, 30, 0.5]]], + "offsets": (0, 0), + "scales": (1, 1), + "rescaled": [[[0, 0, 0.95], [20, 30, 0.5]]], + }, + { + "predictions": [ + [[0, 0, 0.12], [10, 0, 0.5]], # individual 1 + [[1000, 500, 0.24], [50, 250, 0.6]], # individual 2 + ], + "offsets": (5, 7), + "scales": (0.2, 0.5), + "rescaled": [ + [[5, 7, 0.12], [7, 7, 0.5]], # individual 1 + [[205, 257, 0.24], [15, 132, 0.6]], # individual 2 + ], + }, + ], +) +def test_rescale_bottom_up(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bodyparts"], + mode=RescaleAndOffset.Mode.KEYPOINT, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bodyparts": np.array(data["predictions"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bodyparts"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bodyparts"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[222.0, 562.0, 721.0, 637.0]], + "offsets": (0, 0), + "scales": (1, 1), + "rescaled": [[222.0, 562.0, 721.0, 637.0]], + }, + { + "bboxes": [[386.71875, 219.53125, 281.640625, 248.828125]], + "offsets": (-768, 0), + "scales": (2.56, 2.56), + "rescaled": [[222.0, 562.0, 721.0, 637.0]], + }, + { + "bboxes": [ + [0, 0, 100, 100], + [5, 10, 100, 100], + [5, 10, 10, 20], + ], + "offsets": (3, 7), + "scales": (2, 0.5), + "rescaled": [ + [3, 7, 200, 50], + [13, 12, 200, 50], + [13, 12, 20, 10], + ], + }, + ], +) +def test_rescale_detector(data): + """Expects x_processed = x * scale + offset.""" + postprocessor = RescaleAndOffset( + keys_to_rescale=["bboxes"], + mode=RescaleAndOffset.Mode.BBOX_XYWH, + ) + context = {"scales": data["scales"], "offsets": data["offsets"]} + predictions = {"bboxes": np.array(data["bboxes"])} + predictions, context = postprocessor(predictions, context=context) + print(predictions["bboxes"].tolist()) + print(data["rescaled"]) + np.testing.assert_array_equal(predictions["bboxes"], np.array(data["rescaled"])) + + +@pytest.mark.parametrize( + "data", + [ + { + "bodyparts": [ + [[3.1, 1, 0.8], [1, 0, 0.9]], # assembly 1 (x, y, score) + [[2.2, 1.6, 0.5], [3, 3, 0.4]], # assembly 2 (x, y, score) + ], + "id_heatmap": [ # id1, id2 score for each pixel + [[0.1, 0.1], [0.2, 0.1], [0.3, 0.1], [0.4, 0.1]], + [[0.1, 0.2], [0.2, 0.2], [0.3, 0.2], [0.4, 0.2]], + [[0.1, 0.3], [0.2, 0.3], [0.3, 0.3], [0.4, 0.3]], + [[0.1, 0.4], [0.2, 0.4], [0.3, 0.4], [0.4, 0.4]], + ], + "id_scores": [ # id1, id2 score for each bodypart + [[0.4, 0.2], [0.2, 0.1]], # assembly 1 (id_1 proba, id_2 proba) + [[0.3, 0.3], [0.4, 0.4]], # assembly 2 (id_1 proba, id_2 proba) + ], + }, + ], +) +def test_assign_id_scores(data): + p = PredictKeypointIdentities( + identity_key="keypoint_identity", + identity_map_key="identity_map", + pose_key="bodyparts", + keep_id_maps=True, + ) + bodyparts = np.array(data["bodyparts"]) + id_heatmap = np.array(data["id_heatmap"]) + expected_ids = np.array(data["id_scores"]) + print() + print(bodyparts.shape) + print(id_heatmap.shape) + print(expected_ids.shape) + predictions_in = {"bodyparts": bodyparts, "identity_map": id_heatmap} + predictions, _ = p(predictions_in, {}) + np.testing.assert_array_equal( + predictions["keypoint_identity"], + expected_ids, + ) + + +def test_prepare_backbone_features(): + p = PrepareBackboneFeatures(top_down=False) + + img_w, img_h = 256, 128 + features = np.zeros((1, img_h, img_w)) + + features[0, 15, 10] = 1 + features[0, 25, 20] = 2 + features[0, 35, 30] = 3 + + pose = np.array( + [ + [ + [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)) + predictions_out, context_out = p(predictions, context) + + assert len(predictions_out) == 1 + assert len(context_out) == 1 + preds = predictions_out[0] + + assert "backbone" in preds + assert "bodypart_features" in preds["backbone"] + bodypart_features = preds["backbone"]["bodypart_features"] + print(f"Bodypart features: {bodypart_features.shape}") + print(bodypart_features) + assert bodypart_features.shape == (1, 3, 1) + assert bodypart_features.reshape(-1).tolist() == [1, 2, 3] + + +def test_prepare_top_down_backbone_features(): + p = PrepareBackboneFeatures(top_down=True) + + img_w, img_h = 256, 256 + + features = np.zeros((2, 1, img_h, img_w)) + features[0, 0, 15, 10] = 1 + features[0, 0, 25, 20] = 2 + features[0, 0, 35, 30] = 3 + features[1, 0, 95, 10] = 11 + features[1, 0, 85, 20] = 12 + features[1, 0, 75, 30] = 13 + + pose_idv0 = np.array( + [ + [ + [10.1, 15.1, 0.95], + [20.1, 25.1, 0.95], + [29.9, 34.9, 0.95], + ], + ] + ) + pose_idv1 = np.array( + [ + [ + [10.1, 95.1, 0.95], + [20.1, 85.1, 0.95], + [29.9, 74.9, 0.95], + ], + ] + ) + + predictions = [ + dict(backbone=dict(features=features[0]), bodypart=dict(poses=pose_idv0)), + dict(backbone=dict(features=features[1]), bodypart=dict(poses=pose_idv1)), + ] + context = dict(top_down_crop_size=(img_w, img_h)) + predictions_out, context_out = p(predictions, context) + + assert len(predictions_out) == 2 + assert len(context_out) == 1 + 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"] + print(f"Bodypart features: {bodypart_features.shape}") + print(bodypart_features) + assert bodypart_features.shape == (1, 3, 1) + assert bodypart_features.reshape(-1).tolist() == expected + + +@pytest.mark.parametrize( + "data", + [ + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30], [40, 40, 50, 50]], + "bbox_scores": [0.1, 0.5, 0.9], + "threshold": 0.3, + "expected_bboxes": [[20, 20, 30, 30], [40, 40, 50, 50]], + "expected_scores": [0.5, 0.9], + }, + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30], [40, 40, 50, 50]], + "bbox_scores": [0.1, 0.2, 0.3], + "threshold": 0.5, + "expected_bboxes": [], + "expected_scores": [], + }, + { + "bboxes": [[0, 0, 10, 10], [20, 20, 30, 30]], + "bbox_scores": [0.3, 0.7], + "threshold": 0.3, + "expected_bboxes": [[0, 0, 10, 10], [20, 20, 30, 30]], + "expected_scores": [0.3, 0.7], + }, + { + "bboxes": [], + "bbox_scores": [], + "threshold": 0.5, + "expected_bboxes": [], + "expected_scores": [], + }, + ], +) +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) + + +def test_predict_keypoint_identities_handles_nan_keypoints(): + import warnings + + p = PredictKeypointIdentities( + identity_key="keypoint_identity", + identity_map_key="identity_map", + pose_key="bodyparts", + keep_id_maps=True, + ) + + # PAF-style output: (num_individuals, num_bodyparts, 5); missing joint is all-NaN + bodyparts = np.array( + [ + [ + [3.1, 1.0, 0.8, 0.0, 0.5], # valid + [np.nan, np.nan, np.nan, np.nan, np.nan], # missing (assembler default) + [1.0, 0.0, 0.9, 1.0, 0.5], # valid + ], + ] + ) + id_heatmap = np.array( + [ + [[0.1, 0.1], [0.2, 0.1], [0.3, 0.1], [0.4, 0.1]], + [[0.1, 0.2], [0.2, 0.2], [0.3, 0.2], [0.4, 0.2]], + [[0.1, 0.3], [0.2, 0.3], [0.3, 0.3], [0.4, 0.3]], + [[0.1, 0.4], [0.2, 0.4], [0.3, 0.4], [0.4, 0.4]], + ] + ) + predictions_in = {"bodyparts": bodyparts, "identity_map": id_heatmap} + + with warnings.catch_warnings(): + warnings.simplefilter("error", RuntimeWarning) + predictions, _ = p(predictions_in, {}) + + expected = np.zeros((1, 3, 2)) + expected[0, 0] = id_heatmap[1, 3] # rint(3.1, 1.0) -> (3, 1) + expected[0, 1] = 0.0 # NaN keypoint: leave identity scores at zero + expected[0, 2] = id_heatmap[0, 1] # rint(1.0, 0.0) -> (1, 0) + + np.testing.assert_array_equal(predictions["keypoint_identity"], expected) diff --git a/tests/pose_estimation_pytorch/data/test_preprocessor.py b/tests/pose_estimation_pytorch/data/test_preprocessor.py new file mode 100644 index 0000000000..9a68d76fe7 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_preprocessor.py @@ -0,0 +1,158 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the pre-processors.""" + +import albumentations as A +import numpy as np +import pytest + +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( + "data", + [ + { + "image_shape": (2, 4, 4), + "resize_transform": {"height": 5, "width": 4, "keep_ratio": True}, + "output_shape": (2, 4, 4), + "padded_shape": (5, 4, 4), # single offset as not a batch + "output_context": {"offsets": (0, 0), "scales": (1, 1)}, + }, + { + "image_shape": (1, 2, 4, 4), # as batch + "resize_transform": {"height": 10, "width": 4, "keep_ratio": True}, + "output_shape": (1, 2, 4, 4), + "padded_shape": (1, 10, 4, 4), + "output_context": {"offsets": [(0, 0)], "scales": [(1, 1)]}, + }, + { + "image_shape": (2, 4, 3), + "resize_transform": {"height": 10, "width": 8, "keep_ratio": True}, + "output_shape": (4, 8, 3), + "padded_shape": (10, 8, 3), + "output_context": {"offsets": (0, 0), "scales": (0.5, 0.5)}, + }, + ], +) +def test_augment_image_rescaling(data): + resize_transform = build_resize_transforms(data["resize_transform"]) + transform = A.Compose( + resize_transform, + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + preprocessor = AugmentImage(transform) + img = np.ones(data["image_shape"]) + transformed_image, context = preprocessor(img, context={}) + print() + print(transformed_image[:, :, 0]) # first channel + print(context) + assert np.sum(transformed_image) == np.sum(np.ones(data["output_shape"])) + assert context == data["output_context"] + assert transformed_image.shape == data["padded_shape"] + + +ctd_preprocessor = build_conditional_top_down_preprocessor( + color_mode="RGB", + transform=A.Compose( + build_resize_transforms({"height": 100, "width": 100, "keep_ratio": True}), + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ), + bbox_margin=0, + top_down_crop_size=(256, 256), +) + + +@pytest.mark.parametrize( + "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]]])}, + "output_context": { + "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)], + }, + }, + # 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]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + # 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]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + # 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]]])}, + "output_context": { + "cond_kpts": np.array( + [ + [[10, 10, 0.8], [20, 20, 0.8]], + ] + ), + "bboxes": [np.array([10, 10, 10, 10])], + "offsets": [(10, 10)], + "scales": [(0.1, 0.1)], + }, + }, + ], +) +def test_conditional_top_down_preprocessor(data): + input_img = np.ones(data["image_shape"]) + + 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]) + + +def deep_equal(a, b): + if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): + return np.array_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, strict=False)) + else: + return a == b diff --git a/tests/pose_estimation_pytorch/data/test_transforms.py b/tests/pose_estimation_pytorch/data/test_transforms.py new file mode 100644 index 0000000000..f85ce00ffe --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_transforms.py @@ -0,0 +1,302 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the custom transforms.""" + +import random + +import albumentations as A +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data import transforms + + +@pytest.mark.parametrize( + "height, width, image_shapes", + [ + (200, 200, [(300, 300, 3), (1000, 1000, 3), (1024, 1024, 1)]), + (512, 512, [(1024, 1024, 3), (128, 128, 4), (300, 300, 1)]), + (1024, 512, [(600, 300, 3), (4096, 2048, 3), (50, 25, 1)]), + (800, 1300, [(80, 130, 3), (1600, 2600, 4), (1200, 1950, 1)]), + ], +) +def test_dlc_resize_pad_good_aspect_ratio(height, width, image_shapes): + aug = transforms.KeepAspectRatioResize(width=width, height=height, mode="pad") + for image_shape in image_shapes: + fake_image = np.zeros(image_shape) + transformed = aug(image=fake_image, keypoints=[]) + assert transformed["image"].shape[:2] == (height, width) + assert transformed["image"].shape[2] == fake_image.shape[2] + + +@pytest.mark.parametrize( + "data", + [ + { + "height": 200, + "width": 200, + "in_shapes": [(100, 50, 3), (50, 400, 3)], + "out_shapes": [(200, 100, 3), (25, 200, 3)], + }, + { + "height": 128, + "width": 256, + "in_shapes": [(100, 100, 3), (512, 256, 3)], + "out_shapes": [(128, 128, 3), (128, 64, 3)], + }, + ], +) +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"], strict=False): + fake_image = np.zeros(in_shape) + transformed = aug(image=fake_image, keypoints=[]) + assert transformed["image"].shape == out_shape + + +@pytest.mark.parametrize( + "data", + [ + { + "height": 200, + "width": 200, + "in_shape": (100, 50, 3), + "out_shape": (200, 100, 3), + "in_keypoints": [(50.0, 50.0), (25.0, 10.0)], + "out_keypoints": [(100.0, 100.0), (50.0, 20.0)], + }, + { + "height": 512, + "width": 256, + "in_shape": (1024, 1024, 3), + "out_shape": (256, 256, 3), + "in_keypoints": [(512.0, 512.0), (100.0, 10.0)], + "out_keypoints": [(128.0, 128.0), (25.0, 2.5)], + }, + ], +) +def test_dlc_resize_pad_bad_aspect_ratio_with_keypoints(data): + aug = transforms.KeepAspectRatioResize(width=data["width"], height=data["height"], mode="pad") + transform = A.Compose( + [aug], + keypoint_params=A.KeypointParams("xy", remove_invisible=False), + ) + fake_image = np.zeros(data["in_shape"]) + transformed = transform(image=fake_image, keypoints=data["in_keypoints"]) + assert transformed["image"].shape == data["out_shape"] + assert transformed["keypoints"] == data["out_keypoints"] + + +def test_coarse_dropout(): + transforms.CoarseDropout( + max_holes=10, + max_height=0.05, + min_height=0.01, + max_width=0.05, + min_width=0.01, + p=0.5, + ) + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=10.0, + shift_prob=0.0, + scale_factor=[0.1, 2.0], + scale_prob=0.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=1.0, + scale_factor=[1.0, 1.0], + scale_prob=1.0, + sampling="uniform", # truncnorm throws an error if delta is 0 + ), + }, + ], +) +def test_random_bbox_transform_does_not_modify_with_base_config(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + 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)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + bboxes_out = np.asarray(output["bboxes"]) + print("bboxes") + print(bboxes_out) + print() + np.testing.assert_array_almost_equal(bboxes, bboxes_out) + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.25, 0.5], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[1.0, 1.5], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.5, 1.25], + scale_prob=1.0, + ), + }, + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.0, + shift_prob=0.0, + scale_factor=[0.5, 1.5], + scale_prob=0.5, + ), + }, + ], +) +def test_random_bbox_transform_scale(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + 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)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + + 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:], 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 + h_low, h_high = h * scale_low, h * scale_high + print("(w, w_low, w_high)", w, w_low, w_high) + print("(h, h_low, h_high)", h, h_low, h_high) + assert w_low <= bbox_out_wh[0].item() <= w_high + assert h_low <= bbox_out_wh[1].item() <= h_high + + +@pytest.mark.parametrize( + "data", + [ + { + "image_shape": [480, 640, 3], + "transform_config": dict( + shift_factor=0.1, + shift_prob=1.0, + scale_factor=[1.0, 1.0], + scale_prob=0.0, + ), + }, + ], +) +def test_random_bbox_transform_shift(data: dict) -> None: + _set_random_seed() + h, w, c = data["image_shape"] + + # generate 100 bboxes + bboxes = _gen_random_bboxes(np.random.default_rng(seed=0), 100, w, h) + + t = A.Compose( + [transforms.RandomBBoxTransform(**data["transform_config"])], + 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)), + ) + print("Output bounding boxes") + for out_bbox in output["bboxes"]: + print(out_bbox) + print() + + bboxes_out = np.asarray(output["bboxes"]) + shift = data["transform_config"]["shift_factor"] + 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 + max_shift_x, max_shift_y = w * shift, h * shift + assert x - max_shift_x <= x_out <= x + max_shift_x + assert y - max_shift_y <= y_out <= y + max_shift_y + + +def _set_random_seed(): + np.random.seed(0) + random.seed(0) + + +def _gen_random_bboxes( + 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)) + # sample x, y in the images + bboxes[:, :2] = image_wh * gen.random((num_bboxes, 2)) + # sample w, h with the space remaining + bboxes[:, 2:] = (image_wh - bboxes[:, :2]) * gen.random((num_bboxes, 2)) + + print() + print("Input bounding boxes") + print(bboxes) + return bboxes diff --git a/tests/pose_estimation_pytorch/data/test_utils.py b/tests/pose_estimation_pytorch/data/test_utils.py new file mode 100644 index 0000000000..1494e01787 --- /dev/null +++ b/tests/pose_estimation_pytorch/data/test_utils.py @@ -0,0 +1,97 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests data utils.""" + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.data.utils as utils + + +@pytest.mark.parametrize( + "keypoints, expected_bboxes, params", + [ + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 10, 5], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [[-1, -1, 2], [3, 4, 2]], + [0, 0, 3, 4], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 5, 3], + dict(image_w=5, image_h=3, margin=0), + ), + ( + [[0, 0, 2], [10, 5, 2]], + [0, 0, 5, 3], + dict(image_w=5, image_h=3, margin=10), + ), + ( + [[[0, 0, 2], [10, 5, 2]]], + [[0, 0, 10, 5]], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [4, 1, 6, 4], + [1, 3, 6, 9], + ], + dict(image_w=1024, image_h=1024, margin=0), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [2, 0, 10, 7], + [0, 1, 9, 13], + ], + dict(image_w=1024, image_h=1024, margin=2), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 2], [2, 0, -1], [1, 12, 2]], + ], + [ + [2, 0, 8, 7], + [0, 1, 9, 9], + ], + dict(image_w=10, image_h=10, margin=2), + ), + ( + [ + [[4, 1, 2], [10, 5, 2], [3, 12, 0]], + [[7, 3, 0], [2, 0, -1], [1, 12, 0]], + ], + [ + [2, 0, 8, 7], + [0, 0, 0, 0], + ], + dict(image_w=10, image_h=10, margin=2), + ), + ], +) +def test_bbox_from_keypoints(keypoints, expected_bboxes, params): + keypoints = np.asarray(keypoints, dtype=float) + bboxes = utils.bbox_from_keypoints(keypoints, **params) + expected_bboxes = np.asarray(expected_bboxes, dtype=float) + np.testing.assert_array_almost_equal(bboxes, expected_bboxes) 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 new file mode 100644 index 0000000000..7f418f4912 --- /dev/null +++ b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py @@ -0,0 +1,131 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the heatmap target generators (plateau and gaussian)""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( + HeatmapGaussianGenerator, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "dist_thresh": 3, + "num_heatmaps": 1, + "in_shape": (3, 3), + "out_shape": (3, 3), + "centers": [(1, 1)], + "expected_output": [ + [0.7788, 0.8825, 0.7788], + [0.8825, 1.0000, 0.8825], + [0.7788, 0.8825, 0.7788], + ], + }, + { + "dist_thresh": 3, + "num_heatmaps": 1, + "in_shape": (5, 5), + "out_shape": (5, 5), + "centers": [[1, 1], [2, 2]], + "expected_output": [ + [0.7788, 0.8825, 0.7788, 0.5353, 0.3679], + [0.8825, 1.0000, 0.8825, 0.7788, 0.5353], + [0.7788, 0.8825, 1.0000, 0.8825, 0.6065], + [0.5353, 0.7788, 0.8825, 0.7788, 0.5353], + [0.3679, 0.5353, 0.6065, 0.5353, 0.3679], + ], + }, + { + "dist_thresh": 1, + "num_heatmaps": 1, + "in_shape": (4, 4), + "out_shape": (4, 4), + "centers": [[1, 1]], + "expected_output": [ + [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], + ], + }, + ], +) +def test_gaussian_heatmap_generation_single_keypoint(data): + dist_thresh = data["dist_thresh"] + generator = HeatmapGaussianGenerator( + num_heatmaps=data["num_heatmaps"], + pos_dist_thresh=dist_thresh, + heatmap_mode=HeatmapGaussianGenerator.Mode.KEYPOINT, + generate_locref=False, + ) + stride = data["in_shape"][0] / data["out_shape"][0] + outputs = torch.zeros((1, data["num_heatmaps"], *data["out_shape"])) + ann_shape = (1, len(data["centers"]), data["num_heatmaps"], 2) + annotations = { + "keypoints": torch.tensor(data["centers"]).reshape(ann_shape) # x, y + } + targets = generator(stride, {"heatmap": outputs}, annotations) + + print("Targets") + print(targets["heatmap"]["target"]) + print() + np.testing.assert_almost_equal( + targets["heatmap"]["target"].cpu().numpy().reshape(data["out_shape"]), + np.array(data["expected_output"]), + decimal=3, + ) + + +@pytest.mark.parametrize( + "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): + # generate annotations + annotations = { + "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))} + + # generate heatmap + generator = HeatmapGaussianGenerator( + num_heatmaps=num_keypoints, + pos_dist_thresh=17, + heatmap_mode=HeatmapGaussianGenerator.Mode.KEYPOINT, + 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]) + + # get coords of max value of the heatmap + gaus_max = torch.argmax(target_heatmap, dim=2) + + # get unraveled coords + x = gaus_max % image_size[1] + y = gaus_max // image_size[1] + + # get heatmap center tensor + predict_kp = torch.stack((x, y), dim=-1) + # Remove num_animals dimension - only one animal is supported + annotations["keypoints"] = torch.squeeze(annotations["keypoints"], dim=1) + + # compare heatmap center to annotation + assert torch.eq(annotations["keypoints"], predict_kp).all().item() 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 new file mode 100644 index 0000000000..d335fc5524 --- /dev/null +++ b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py @@ -0,0 +1,90 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the heatmap target generators (plateau and gaussian)""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( + HeatmapGenerator, + HeatmapPlateauGenerator, +) + + +@pytest.mark.parametrize( + "data", + [ + { + "dist_thresh": 1, + "num_heatmaps": 1, + "in_shape": (3, 3), + "out_shape": (3, 3), + "centers": [(1, 1)], + "expected_output": [ + [0.0, 1.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 1.0, 0.0], + ], + }, + { + "dist_thresh": 2, + "num_heatmaps": 1, + "in_shape": (5, 5), + "out_shape": (5, 5), + "centers": [[1, 1], [2, 2]], + "expected_output": [ + [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], + ], + }, + { + "dist_thresh": 2, + "num_heatmaps": 1, + "in_shape": (4, 4), + "out_shape": (4, 4), + "centers": [[1, 1]], + "expected_output": [ + [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], + ], + }, + ], +) +def test_plateau_heatmap_generation_single_keypoint(data): + dist_thresh = data["dist_thresh"] + generator = HeatmapPlateauGenerator( + num_heatmaps=data["num_heatmaps"], + pos_dist_thresh=dist_thresh, + heatmap_mode=HeatmapGenerator.Mode.KEYPOINT, + generate_locref=False, + ) + stride = data["in_shape"][0] / data["out_shape"][0] + outputs = torch.zeros((1, data["num_heatmaps"], *data["out_shape"])) + ann_shape = (1, len(data["centers"]), data["num_heatmaps"], 2) + annotations = { + "keypoints": torch.tensor(data["centers"]).reshape(ann_shape) # x, y + } + targets = generator(stride, {"heatmap": outputs}, annotations) + + print("Targets") + print(targets["heatmap"]["target"]) + print() + np.testing.assert_almost_equal( + targets["heatmap"]["target"].cpu().numpy().reshape(data["out_shape"]), + np.array(data["expected_output"]), + decimal=3, + ) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_download.py b/tests/pose_estimation_pytorch/modelzoo/test_download.py new file mode 100644 index 0000000000..06cc9857e1 --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_download.py @@ -0,0 +1,36 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import os + +import dlclibrary +import pytest +from dlclibrary.dlcmodelzoo.modelzoo_download import MODELOPTIONS + + +def test_download_huggingface_model(tmp_path_factory, model="full_cat"): + folder = tmp_path_factory.mktemp("temp") + dlclibrary.download_huggingface_model(model, str(folder)) + + assert os.path.exists(folder / "pose_cfg.yaml") + assert any(f.startswith("snapshot-") for f in os.listdir(folder)) + # Verify that the Hugging Face folder was removed + assert not any(f.startswith("models--") for f in os.listdir(folder)) + + +def test_download_huggingface_wrong_model(): + with pytest.raises(ValueError): + dlclibrary.download_huggingface_model("wrong_model_name") + + +@pytest.mark.skip(reason="slow") +@pytest.mark.parametrize("model", MODELOPTIONS) +def test_download_all_models(tmp_path_factory, model): + test_download_huggingface_model(tmp_path_factory, model) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py new file mode 100644 index 0000000000..e7608625fa --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py @@ -0,0 +1,198 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import pathlib +import socket +from types import SimpleNamespace + +import numpy as np +import pandas as pd +import pytest + +fmpose3d = pytest.importorskip("fmpose3d", reason="fmpose3d not installed") +pytestmark = pytest.mark.fmpose3d + +# DLC fmpose_3d modules import fmpose3d; load only after importorskip above. +from deeplabcut.pose_estimation_pytorch.modelzoo.fmpose_3d import inference as fmp_inf # noqa: E402 +from deeplabcut.pose_estimation_pytorch.modelzoo.fmpose_3d.fmpose3d import ( # noqa: E402 + get_fmpose3d_inference_api, +) + + +def _has_network(host="huggingface.co", port=443, timeout=3) -> bool: + """Return True if we can reach *host* (used to download model weights).""" + try: + socket.create_connection((host, port), timeout=timeout).close() + return True + except OSError: + return False + + +requires_network = pytest.mark.skipif( + not _has_network(), + reason="No network connection (needed to download model weights)", +) + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +_EXAMPLE_IMAGE = ( + _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.unittest +def test_api_init(model_type): + api = get_fmpose3d_inference_api(model_type, device="cpu") + assert api is not None + assert hasattr(api, "prepare_2d") + assert hasattr(api, "pose_3d") + assert hasattr(api, "predict") + + +# --------------------------------------------------------------------------- +# Integration: downloads weights and runs inference (needs network) +# --------------------------------------------------------------------------- +@requires_network +@pytest.mark.functional +def test_prepare_2d_and_pose_3d(): + """2D detection followed by 3D lifting on a real image.""" + api = get_fmpose3d_inference_api("fmpose3d_animals", device="cpu") + + result_2d = api.prepare_2d(source=str(_EXAMPLE_IMAGE)) + assert isinstance(result_2d.keypoints, np.ndarray) + assert result_2d.keypoints.shape[-1] == 2 + + keypoints_3d = api.pose_3d( + keypoints_2d=result_2d.keypoints, + image_size=result_2d.image_size, + ) + assert isinstance(keypoints_3d.poses_3d, np.ndarray) + assert keypoints_3d.poses_3d.shape[-1] == 3 + + +@requires_network +@pytest.mark.functional +def test_predict_end_to_end(): + """Full pipeline (2D -> 3D) in a single call.""" + api = get_fmpose3d_inference_api("fmpose3d_animals", device="cpu") + predictions_3d = api.predict(source=str(_EXAMPLE_IMAGE)) + + assert isinstance(predictions_3d.poses_3d, np.ndarray) + assert predictions_3d.poses_3d.shape[-1] == 3 + + +@pytest.mark.unittest +def test_pose2d_to_dlc_predictions_shapes(): + pose_2d = SimpleNamespace( + keypoints=np.random.rand(2, 3, 4, 2).astype(np.float32), + scores=np.random.rand(2, 3, 4).astype(np.float32), + ) + preds = fmp_inf._pose2d_to_dlc_predictions( + pose_2d=pose_2d, + max_individuals=1, + num_bodyparts=4, + ) + + assert len(preds) == 3 + assert preds[0]["bodyparts"].shape == (1, 4, 3) + np.testing.assert_allclose(preds[0]["bodyparts"][0, :, :2], pose_2d.keypoints[0, 0]) + + +@pytest.mark.unittest +def test_poses3d_to_dataframe_layout(): + scorer = "DLC_test" + bodyparts = ["bp1", "bp2", "bp3"] + columns_2d = pd.MultiIndex.from_product( + [[scorer], ["individual1"], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + df_2d = pd.DataFrame(np.zeros((2, len(columns_2d))), columns=columns_2d) + + poses_3d = [ + np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]), + np.array([[10.0, 11.0, 12.0], [13.0, 14.0, 15.0], [16.0, 17.0, 18.0]]), + ] + df_3d = fmp_inf._poses3d_to_dataframe(poses_3d, df_2d, f"{scorer}_3d") + + assert df_3d.columns.names == ["scorer", "bodyparts", "coords"] + assert set(df_3d.columns.get_level_values("coords")) == {"x", "y", "z"} + assert df_3d.loc[0, (f"{scorer}_3d", "bp1", "x")] == 1.0 + assert df_3d.loc[1, (f"{scorer}_3d", "bp3", "z")] == 18.0 + + +@pytest.mark.functional +def test_video_inference_fmpose3d_include_3d_return(tmp_path, monkeypatch): + frames = [np.zeros((8, 8, 3), dtype=np.uint8) for _ in range(2)] + + class FakeVideoIterator: + def __init__(self, _path, cropping=None): + self.dimensions = (8, 8) + self.fps = 30 + self._frames = frames + + def __iter__(self): + return iter(self._frames) + + class FakeAPI: + def prepare_2d(self, source): + n_frames = source.shape[0] + return SimpleNamespace( + keypoints=np.zeros((1, n_frames, 26, 2), dtype=np.float32), + scores=np.ones((1, n_frames, 26), dtype=np.float32), + image_size=(8, 8), + ) + + def pose_3d(self, keypoints_2d, image_size): + n_frames = keypoints_2d.shape[1] + return SimpleNamespace( + 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): + bodyparts = model_cfg["metadata"]["bodyparts"] + individuals = model_cfg["metadata"]["individuals"] + columns = pd.MultiIndex.from_product( + [[dlc_scorer], individuals, bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + return pd.DataFrame(np.zeros((len(predictions), len(columns))), columns=columns) + + monkeypatch.setattr(fmp_inf, "VideoIterator", FakeVideoIterator) + monkeypatch.setattr( + fmp_inf, + "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, + "get_superanimal_colormaps", + lambda: { + "superanimal_quadruped": "viridis", + "superanimal_humanbody": "viridis", + }, + ) + + result = fmp_inf._video_inference_fmpose3d( + video_paths=[str(tmp_path / "dummy.mp4")], + model_name="fmpose3d_animals", + dest_folder=tmp_path, + create_labeled_video=False, + include_3d_in_return=True, + ) + + payload = result[str(tmp_path / "dummy.mp4")] + assert "df_2d" in payload + assert "df_3d" in payload + assert isinstance(payload["df_3d"], pd.DataFrame) + assert (tmp_path / "dummy_DLC_fmpose3d_animals_3d.h5").exists() diff --git a/tests/pose_estimation_pytorch/modelzoo/test_generalized_data_converter_config.py b/tests/pose_estimation_pytorch/modelzoo/test_generalized_data_converter_config.py new file mode 100644 index 0000000000..23463c7ee7 --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_generalized_data_converter_config.py @@ -0,0 +1,162 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests capturing the current state of the generalized_data_converter configuration. + +These tests document default values and behavior so that a migration to a new +configuration system can preserve existing behavior. Add or adjust assertions +if you intentionally change defaults. +""" + +import pytest + +from deeplabcut.core.config import read_config_as_dict, write_config +from deeplabcut.modelzoo.generalized_data_converter.datasets.materialize import ( + MaDLC_config, + SingleDLC_config, + modify_train_test_cfg, +) + +# Modelzoo SingleDLC_config defaults +# 2026-01-29 config version 0, before centralized typed configs were added +SINGLE_DLC_DEFAULTS = { + "Task": "", + "project_path": "", + "scorer": "", + "date": "", + "video_sets": "", + "skeleton": "", + "bodyparts": [], + "start": 0, + "stop": 1, + "numframes2pick": 42, + "skeleton_color": "black", + "pcutoff": 0.6, + "dotsize": 8, + "alphavalue": 0.7, + "colormap": "rainbow", + "TrainingFraction": "", + "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, + "corner2move2": [50, 50], + "move2corner": True, + "identity": False, +} + + +# MaDLC_config defaults; only differences from Single are listed in tests +MA_DLC_DEFAULTS_OVERRIDES = { + "default_augmenter": "multi-animal-imgaug", + "bodyparts": "MULTI!", + "croppedtraining": True, + "multianimalproject": True, +} +MA_DLC_EXTRA_KEYS = {"individuals", "multianimalbodyparts"} + + +class TestModelzooProjectConfigDefaults: + def test_single_dlc_config_defaults(self): + config = SingleDLC_config() + assert set(config.cfg.keys()) == set(SINGLE_DLC_DEFAULTS.keys()) + for key, expected in SINGLE_DLC_DEFAULTS.items(): + assert config.cfg[key] == expected, f"SingleDLC_config.cfg[{key!r}]" + + def test_ma_dlc_config_has_all_single_keys_plus_ma_specific(self): + single_keys = set(SINGLE_DLC_DEFAULTS.keys()) + ma_config = MaDLC_config() + ma_keys = set(ma_config.cfg.keys()) + assert single_keys <= ma_keys + assert MA_DLC_EXTRA_KEYS <= ma_keys + + def test_ma_dlc_config_overrides_vs_single(self): + single_config = SingleDLC_config() + ma_config = MaDLC_config() + for key, expected in MA_DLC_DEFAULTS_OVERRIDES.items(): + assert ma_config.cfg[key] == expected, f"MaDLC_config.cfg[{key!r}]" + assert ma_config.cfg[key] != single_config.cfg[key], f"MaDLC should differ from Single for {key!r}" + + def test_ma_dlc_config_shared_defaults_match_single(self): + """Keys not overridden in Ma should match Single defaults.""" + single_config = SingleDLC_config() + ma_config = MaDLC_config() + for key in SINGLE_DLC_DEFAULTS: + if key in MA_DLC_DEFAULTS_OVERRIDES: + continue + assert ma_config.cfg[key] == single_config.cfg[key], f"MaDLC_config.cfg[{key!r}] should match Single" + + +class TestModelzooCreateProjectConfig: + """Behavior of create_cfg: file location, format, and update semantics.""" + + def test_single_dlc_create_cfg_writes_config_yaml(self, tmp_path): + config = SingleDLC_config() + config.create_cfg(tmp_path, {"Task": "mytask"}) + path = tmp_path / "config.yaml" + assert path.exists() + data = read_config_as_dict(path) + assert data["Task"] == "mytask" + assert data["default_net_type"] == "resnet_50" + + def test_create_cfg_overwrites_with_kwargs(self, tmp_path): + config = SingleDLC_config() + config.create_cfg(tmp_path, {"Task": "mytask", "batch_size": 16}) + path = tmp_path / "config.yaml" + data = read_config_as_dict(path) + assert data["Task"] == "mytask" + assert data["batch_size"] == 16 + + def test_ma_dlc_create_cfg_writes_config_yaml(self, tmp_path): + config = MaDLC_config() + config.create_cfg(tmp_path, {"Task": "matask"}) + path = tmp_path / "config.yaml" + assert path.exists() + data = read_config_as_dict(path) + assert data["Task"] == "matask" + assert data["multianimalproject"] is True + + +class TestModifyTrainTestCfg: + """Behavior of modify_train_test_cfg (train/test pose config updates).""" + + def test_modify_train_test_cfg_requires_existing_config_path(self, tmp_path): + config_path = tmp_path / "config.yaml" + # Non-existent config must not silently succeed + with pytest.raises(FileNotFoundError): + modify_train_test_cfg(config_path) + + def test_modify_train_test_cfg_sets_expected_values(self, tmp_path, monkeypatch): + train_path = tmp_path / "train" / "pytorch_config.yaml" + test_path = tmp_path / "test" / "pose_cfg.yaml" + snapshot_folder = tmp_path / "train" + train_path.parent.mkdir(parents=True) + test_path.parent.mkdir(parents=True) + write_config(train_path, {"batch_size": 1, "multi_stage": False, "gradient_masking": False}) + write_config(test_path, {"batch_size": 1, "multi_stage": False, "gradient_masking": False}) + monkeypatch.setattr( + "deeplabcut.modelzoo.generalized_data_converter.datasets.materialize.compat.return_train_network_path", + lambda *args, **kwargs: (train_path, test_path, snapshot_folder), + ) + modify_train_test_cfg(tmp_path / "config.yaml") # ignored by mock + for path in (train_path, test_path): + data = read_config_as_dict(path) + assert data["multi_stage"] is True + assert data["batch_size"] == 8 + assert data["gradient_masking"] is True diff --git a/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py new file mode 100644 index 0000000000..aeb5131f65 --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py @@ -0,0 +1,179 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +import copy +from types import SimpleNamespace + +import pytest + +import deeplabcut.pose_estimation_pytorch.modelzoo.inference_helpers as helpers + + +def _dummy_cfg(method: str = "TD") -> dict: + return { + "method": method, + "metadata": {"bodyparts": ["nose"], "unique_bodyparts": []}, + } + + +def test_create_superanimal_inference_runners_uses_custom_config_path(monkeypatch): + cfg = _dummy_cfg("TD") + read_calls = [] + + def fake_from_any(config): + read_calls.append(config) + return cfg + + monkeypatch.setattr(helpers.PoseConfig, "from_any", fake_from_any) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", "det_runner"), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path="det.pt", + ), + ) + + pose_runner, detector_runner, model_cfg = helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config="/tmp/custom_model_cfg.yaml", + ) + + assert read_calls == ["/tmp/custom_model_cfg.yaml"] + assert pose_runner == "pose_runner" + assert detector_runner == "det_runner" + assert model_cfg is cfg + + +def test_create_superanimal_inference_runners_does_not_mutate_custom_dict(monkeypatch): + custom_cfg = _dummy_cfg("TD") + original_bodyparts = list(custom_cfg["metadata"]["bodyparts"]) + + monkeypatch.setattr( + helpers.PoseConfig, + "from_any", + lambda config: copy.deepcopy(config), + ) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", None), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path=None, + ), + ) + + _, _, model_cfg = helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name=None, + customized_model_config=custom_cfg, + ) + + assert custom_cfg["metadata"]["bodyparts"] == original_bodyparts + assert model_cfg is not custom_cfg + + +@pytest.mark.parametrize("input_device", ["auto", None]) +def test_create_superanimal_inference_runners_auto_device_selection(monkeypatch, input_device): + captured = {} + + def fake_build_for_superanimal_inference( + cls, + super_animal, + *, + model_name, + detector_name=None, + max_individuals=30, + device=None, + ): + captured["device"] = device + return _dummy_cfg("TD") + + monkeypatch.setattr( + helpers.PoseConfig, + "build_for_superanimal_inference", + classmethod(fake_build_for_superanimal_inference), + ) + monkeypatch.setattr( + helpers, + "get_inference_runners", + lambda **kwargs: ("pose_runner", "det_runner"), + ) + + import deeplabcut.modelzoo.weight_initialization as wi + + monkeypatch.setattr( + wi, + "build_weight_init", + lambda **kwargs: SimpleNamespace( + snapshot_path="pose.pt", + detector_snapshot_path="det.pt", + ), + ) + + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config=None, + device=input_device, + ) + assert captured["device"] == "auto" + + +def test_create_superanimal_inference_runners_raises_for_fmpose3d(): + with pytest.raises(NotImplementedError, match="FMPose3D"): + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_quadruped", + model_name="FMPose3D_resnet", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config=_dummy_cfg("TD"), + ) + + +def test_create_superanimal_inference_runners_propagates_unsupported_dataset_error( + monkeypatch, +): + def fake_build_for_superanimal_inference(cls, *args, **kwargs): + raise ValueError("Unsupported dataset for model zoo config") + + monkeypatch.setattr( + helpers.PoseConfig, + "build_for_superanimal_inference", + classmethod(fake_build_for_superanimal_inference), + ) + + with pytest.raises(ValueError, match="Unsupported dataset"): + helpers.create_superanimal_inference_runners( + superanimal_name="superanimal_unknown", + model_name="hrnet_w32", + detector_name="fasterrcnn_resnet50_fpn_v2", + customized_model_config=None, + ) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py new file mode 100644 index 0000000000..fab8e2fb3f --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py @@ -0,0 +1,31 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import dlclibrary +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.modelzoo import get_super_animal_snapshot_path + + +@pytest.mark.skip(reason="require-models") +def test_load_superanimal_models_weights_only(): + super_animal_names = dlclibrary.get_available_datasets() + for super_animal in super_animal_names: + print(f"\nTesting {super_animal}") + 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) + + 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) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py new file mode 100644 index 0000000000..544502dee8 --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py @@ -0,0 +1,36 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +import pytest + +import deeplabcut.pose_estimation_pytorch.modelzoo as modelzoo +from deeplabcut.pose_estimation_pytorch.config.pose import PoseConfig + +# 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("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): + model_config = modelzoo.load_super_animal_config( + super_animal=super_animal, + model_name=model_name, + detector_name=detector_name, + ) + + assert isinstance(model_config, PoseConfig) + if detector_name is None: + assert model_config["method"].lower() == "bu" + assert model_config.detector is None + else: + assert model_config["method"].lower() == "td" + assert model_config.detector is not None diff --git a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py new file mode 100644 index 0000000000..2390b12a0c --- /dev/null +++ b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py @@ -0,0 +1,70 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +import numpy as np +import pytest + +from deeplabcut.modelzoo.webapp.inference import SuperanimalPyTorchInference +from deeplabcut.pose_estimation_pytorch.config.pose import PoseConfig +from deeplabcut.utils import auxiliaryfunctions + +# 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("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) + + assert isinstance(inference_pipeline.config, PoseConfig) + assert inference_pipeline.config["metadata"]["bodyparts"] + assert len(inference_pipeline.config["metadata"]["bodyparts"]) > 0 + + +@pytest.mark.skip(reason="require-models") +@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) + 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" + + inference_pipeline.initialize_models(snapshot_path, detector_path) + + assert inference_pipeline.models.pose_runner + assert inference_pipeline.models.detector_runner + + +@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("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) + image_path = "img0001.png" + 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" + + inference_pipeline.initialize_models(snapshot_path, detector_path) + frame = {image_path: np.random.rand(100, 100, 3)} + response = inference_pipeline.predict(frame) + + assert isinstance(response, dict) + assert response["joint_names"] == inference_pipeline.config["bodyparts"] + assert response["predictions"][0]["markers"].shape == ( + max_individuals, + len(inference_pipeline.config["bodyparts"]), + 3, + ) + assert response["predictions"][0]["image_path"] == image_path diff --git a/tests/pose_estimation_pytorch/other/test_api_utils.py b/tests/pose_estimation_pytorch/other/test_api_utils.py new file mode 100644 index 0000000000..0efbfbb52c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_api_utils.py @@ -0,0 +1,94 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import random + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.data.transforms as transforms + +transform_dicts = [ + {"auto_padding": {"pad_height_divisor": 64, "pad_width_divisor": 27}}, + {"resize": {"height": 512, "width": 256, "keep_ration": True}}, + { + "covering": True, + "gaussian_noise": 12.75, + "hist_eq": True, + "motion_blur": True, + "normalize_images": True, + "rotation": 30, + "scale_jitter": [0.5, 1.25], + "auto_padding": {"pad_width_divisor": 64, "pad_height_divisor": 27}, + }, + { + "covering": True, + "gaussian_noise": 100, + "hist_eq": True, + "motion_blur": True, + "normalize_images": True, + "rotation": 180, + "scale_jitter": [0.03, 20], + "auto_padding": {"pad_width_divisor": 64, "pad_height_divisor": 27}, + }, +] + + +def _get_random_params(transform_idx): + return ( + transform_dicts[transform_idx], + (random.randint(100, 1000), random.randint(100, 1000)), + random.randint(1, 100), + random.randint(1, 100), + ) + + +@pytest.mark.parametrize( + "transform_dict, size_image, num_keypoints, num_animals", + [_get_random_params(i) for i in range(4)], +) +def test_build_transforms(transform_dict, size_image, num_keypoints, num_animals): + transform_bbox_aug = transforms.build_transforms(transform_dict) + w, h = size_image + for i in range(10): + test_image = np.random.randint(0, 255, (h, w, 3), dtype=np.uint8) + bboxes = np.random.randint(0, min(w - 1, h - 1), (num_animals, 4)) + bboxes[:, 2] = w - bboxes[:, 0] + bboxes[:, 3] = h - bboxes[:, 1] + keypoints = np.random.randint(0, min(w, h), (num_keypoints, 2)) + + 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, + keypoints=keypoints.copy(), + bboxes=bboxes.copy(), + bbox_labels=np.arange(num_animals), + class_labels=[0 for _ in range(len(keypoints))], + ) + + if "resize" in transform_dict.keys(): + assert transformed_with_bbox["image"].shape[:2] == ( + transform_dict["resize"]["height"], + transform_dict["resize"]["width"], + ) + + if "auto_padding" in transform_dict.keys(): + modh, modw = ( + transform_dict["auto_padding"]["pad_height_divisor"], + transform_dict["auto_padding"]["pad_width_divisor"], + ) + assert transformed_with_bbox["image"].shape[0] % modh == 0 + assert transformed_with_bbox["image"].shape[1] % modw == 0 + + assert len(transformed_with_bbox["keypoints"]) == len(keypoints) diff --git a/tests/pose_estimation_pytorch/other/test_configs/config.yaml b/tests/pose_estimation_pytorch/other/test_configs/config.yaml new file mode 100644 index 0000000000..15ad6f4678 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/config.yaml @@ -0,0 +1,106 @@ + # Project definitions (do not edit) +Task: openfield +scorer: Pranav +date: Aug20 +multianimalproject: false +identity: + + # Project path (change when moving around) +project_path: /home/quentin/datasets/Openfield_pytorch + + # Annotation data set configuration (and individual video cropping parameters) +video_sets: + /Data/openfield-Pranav-2018-08-20/videos/m1s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m1s2.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m2s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m3s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m3s2.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m4s1.mp4: + crop: 0, 640, 0, 480 + /Data/openfield-Pranav-2018-08-20/videos/m5s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m6s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m6s2.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s1.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s2.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m7s3.mp4: + crop: 0, 800, 0, 800 + /Data/openfield-Pranav-2018-08-20/videos/m8s1.mp4: + crop: 0, 800, 0, 800 + + /Users/mwmathis/Downloads/ARCricket1.avi: + crop: 0, 720, 0, 540 +bodyparts: +- snout +- leftear +- rightear +- tailbase + +flipped_keypoints: +- 0 +- 2 +- 1 +- 3 + + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement + + # Fraction of video to start/stop when extracting frames for labeling/refinement +start: 0 +stop: 1 +numframes2pick: 20 + + # Plotting configuration +skeleton: [] +skeleton_color: black +pcutoff: 0.4 +dotsize: 8 +alphavalue: 0.7 +colormap: jet + + # Training,Evaluation and Analysis configuration +TrainingFraction: +- 0.95 +iteration: 1 +default_net_type: resnet_50 +default_augmenter: default +snapshotindex: -1 +batch_size: 1 + + # Cropping Parameters (for analysis and outlier frame detection) +cropping: false + #if cropping is true for analysis, then set the values here: +x1: 0 +x2: 640 +y1: 277 +y2: 624 + + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +- 50 +- 50 +move2corner: true +croppedtraining: diff --git a/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml b/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml new file mode 100644 index 0000000000..ec41492bd4 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/pose_cfg.yaml @@ -0,0 +1,115 @@ + # Project definitions (do not edit) +Task: +scorer: +date: +multianimalproject: +identity: + + # Project path (change when moving around) +project_path: /home/quentin/datasets/Openfield_pytorch/dlc-models/iteration-1/openfieldAug20-trainset95shuffle1/train + + # Annotation data set configuration (and individual video cropping parameters) +video_sets: +bodyparts: + + # Fraction of video to start/stop when extracting frames for labeling/refinement +start: +stop: +numframes2pick: + + # Plotting configuration +skeleton: [] +skeleton_color: black +pcutoff: +dotsize: +alphavalue: +colormap: + + # Training,Evaluation and Analysis configuration +TrainingFraction: +iteration: +default_net_type: +default_augmenter: +snapshotindex: +batch_size: 1 + + # Cropping Parameters (for analysis and outlier frame detection) +cropping: + #if cropping is true for analysis, then set the values here: +x1: +x2: +y1: +y2: + + # Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) +corner2move2: +move2corner: +all_joints: +- - 0 +- - 1 +- - 2 +- - 3 +all_joints_names: +- snout +- leftear +- rightear +- tailbase +alpha_r: 0.02 +apply_prob: 0.5 +contrast: + clahe: true + claheratio: 0.1 + histeq: true + histeqratio: 0.1 +convolution: + edge: false + emboss: + alpha: + - 0.0 + - 1.0 + strength: + - 0.5 + - 1.5 + embossratio: 0.1 + sharpen: false + sharpenratio: 0.3 +cropratio: 0.4 +dataset: training-datasets/iteration-1/UnaugmentedDataSet_openfieldAug20/openfield_Pranav95shuffle1.mat +dataset_type: default +decay_steps: 30000 +display_iters: 1000 +global_scale: 0.8 +init_weights: /home/quentin/miniconda/envs/DEEPLABCUT/lib/python3.8/site-packages/deeplabcut/pose_estimation_tensorflow/models/pretrained/resnet_v1_50.ckpt +intermediate_supervision: false +intermediate_supervision_layer: 12 +location_refinement: true +locref_huber_loss: true +locref_loss_weight: 0.05 +locref_stdev: 7.2801 +lr_init: 0.0005 +max_input_size: 1500 +metadataset: training-datasets/iteration-1/UnaugmentedDataSet_openfieldAug20/Documentation_data-openfield_95shuffle1.pickle +min_input_size: 64 +mirror: false +multi_stage: false +multi_step: +- - 0.005 + - 10000 +- - 0.02 + - 430000 +- - 0.002 + - 730000 +- - 0.001 + - 1030000 +net_type: resnet_50 +num_joints: 4 +pairwise_huber_loss: false +pairwise_predict: false +partaffinityfield_predict: false +pos_dist_thresh: 17 +rotation: 25 +rotratio: 0.4 +save_iters: 50000 +scale_jitter_lo: 0.5 +scale_jitter_up: 1.25 +scmap_type: plateau diff --git a/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml b/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml new file mode 100644 index 0000000000..0be2ca0ed8 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_configs/pytorch_config.yaml @@ -0,0 +1,45 @@ +project_root: /home/quentin/datasets/Openfield_pytorch +pose_cfg_path: /home/quentin/datasets/Openfield_pytorch/dlc-models/iteration-1/openfieldAug20-trainset95shuffle1/train/pose_cfg.yaml +cfg_path: /home/quentin/datasets/Openfield_pytorch/config.yaml + +seed: 42 +device: 'cuda:2' #needs to be updated dynamically; some users might have CPUs +model: + backbone: + type: 'ResNet' + pretrained: 'https://download.pytorch.org/models/resnet50-19c8e357.pth' + heatmap_head: + type: 'SimpleHead' + channels: [ 2048, 1024, 4 ] + kernel_size: [ 2, 2 ] + strides: [ 2, 2 ] + locref_head: + type: 'SimpleHead' + channels: [ 2048, 1024, 8 ] + kernel_size: [ 2, 2 ] + strides: [ 2, 2 ] + pose_model: + stride: 8 + heatmap_type: 'plateau' +optimizer: + type: 'SGD' + params: + lr: 0.005 +scheduler: + type: "LRListScheduler" + params: + milestones : [10, 430] + lr_list : [[0.02], [0.002]] +criterion: + type: 'PoseLoss' + loss_weight_locref: 0.1 + locref_huber_loss: True +#logger: +# type: 'WandbLogger' +# project_name: 'deeplabcut' +# run_name: 'tmp' +solver: + type: 'BottomUpSingleAnimalSolver' +pos_dist_thresh : 17 +batch_size: 1 +epochs: 600 diff --git a/tests/pose_estimation_pytorch/other/test_custom_transforms.py b/tests/pose_estimation_pytorch/other/test_custom_transforms.py new file mode 100644 index 0000000000..28ddb2ffc7 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_custom_transforms.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.data import transforms + + +@pytest.mark.parametrize("width, height", [(200, 200), (300, 300), (400, 400)]) +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") + 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 + assert len(transformed["keypoints"]) + + +def test_grayscale(): + fake_image = np.ones((600, 600, 3)) + fake_image *= np.random.uniform(0, 255, size=fake_image.shape) + fake_image = fake_image.astype(np.uint8) + gray = transforms.Grayscale(alpha=1, p=1) + aug_image = gray(image=fake_image)["image"] + assert aug_image.shape == fake_image.shape + + gray = transforms.Grayscale(alpha=0, p=1) + aug_image = gray(image=fake_image)["image"] + assert np.allclose(fake_image, aug_image) + + with pytest.warns(UserWarning, match="clipped"): + gray = transforms.Grayscale(alpha=1.5) + assert gray.alpha == 1 + + +def test_coarse_dropout(): + fake_image = np.ones((300, 300, 3)) + fake_image *= np.random.uniform(0, 255, size=fake_image.shape) + fake_image = fake_image.astype(np.uint8) + cd = transforms.CoarseDropout(max_height=0.9999, max_width=0.9999, p=1) + kpts = np.random.rand(10, 2) * 298 + 1 + aug_kpts = cd(image=fake_image, keypoints=kpts)["keypoints"] + assert len(aug_kpts) == kpts.shape[0] + assert np.isnan([c for kpt in aug_kpts for c in kpt]).all() diff --git a/tests/pose_estimation_pytorch/other/test_data_helper.py b/tests/pose_estimation_pytorch/other/test_data_helper.py new file mode 100644 index 0000000000..a2e06b3abb --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_data_helper.py @@ -0,0 +1,94 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from __future__ import annotations + +import os +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 + + +def mock_aux() -> Mock: + aux_functions = Mock() + aux_functions.read_plainconfig = Mock() + aux_functions.read_plainconfig.return_value = {} + return aux_functions + + +@patch("deeplabcut.pose_estimation_pytorch.data.base.auxiliaryfunctions", mock_aux()) +def _get_loader(project_root): + if not (Path(project_root) / "training-datasets").exists(): + create_training_dataset(config=str(Path(project_root) / "config.yaml")) + return DLCLoader(Path(project_root) / "config.yaml", shuffle=1) + + +@pytest.mark.skip(reason="This behaviour is not implemented yet") +@pytest.mark.parametrize("repo_path", ["/home/anastasiia/DLCdev"]) +def test_propertymeta_project(repo_path): + project_root = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + dlc_loader = _get_loader(project_root) + + for prop in dlc_loader.properties: + print(prop, getattr(dlc_loader, prop)) + + +@pytest.mark.skip(reason="This behaviour is not implemented yet") +@pytest.mark.parametrize( + "repo_path, mode", + [("/home/anastasiia/DLCdev", "train"), ("/home/anastasiia/DLCdev", "test")], +) +def test_propertymeta_dataset(repo_path, mode): + repo_path = "/home/anastasiia/DLCdev" + mode = "train" + project_root = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + dlc_loader = _get_loader(project_root) + dataset = dlc_loader.create_dataset(transform=None, mode=mode) + + for prop in dataset.properties: + print(prop, getattr(dataset, prop)) + + +@pytest.mark.parametrize( + "list_dicts, keys_to_include", + [ + ([{"a": 1, "b": 2}, {"a": 3, "b": 4}], ["a"]), + ( + [ + *[ + { + "keypoints": np.random.randn(27, 3), + "images": np.random.randn(256, 192), + } + ] + * 10 + ], + [*["keypoints", "images"] * 10], + ), + ], +) +def test_merge_list_of_dicts(list_dicts, keys_to_include): + result_dict = merge_list_of_dicts(list_dicts, keys_to_include) + expected_result_dict = {} + for dictionary in list_dicts: + for key in dictionary: + if key not in keys_to_include: + continue + else: + if key not in expected_result_dict: + expected_result_dict[key] = [] + expected_result_dict[key].append(dictionary[key]) + assert result_dict == expected_result_dict diff --git a/tests/pose_estimation_pytorch/other/test_dataset.py b/tests/pose_estimation_pytorch/other/test_dataset.py new file mode 100644 index 0000000000..9185523843 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_dataset.py @@ -0,0 +1,166 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import os +import random +from pathlib import Path + +import albumentations as A +import pytest +from torch.utils.data import DataLoader + +import deeplabcut.pose_estimation_pytorch as dlc +import deeplabcut.utils.auxiliaryfunctions as dlc_auxfun +from deeplabcut.core.engine import Engine +from deeplabcut.generate_training_dataset import create_training_dataset + + +def _get_dataset(path, transform, mode="train"): + project_root = Path(path) + if not (project_root / "training-datasets").exists(): + print(str(project_root / "config.yaml")) + create_training_dataset( + config=str(project_root / "config.yaml"), + net_type="resnet_50", + engine=Engine.PYTORCH, + ) + + loader = dlc.DLCLoader(Path(project_root) / "config.yaml", shuffle=1) + dataset = loader.create_dataset(transform=transform, mode=mode) + return dataset + + +def _get_openfield_dataset(transform=None): + dlc_path = dlc_auxfun.get_deeplabcut_path() + repo_path = os.path.dirname(dlc_path) + openfield_path = os.path.join(repo_path, "examples", "openfield-Pranav-2018-10-30") + + return _get_dataset(openfield_path, transform=transform) + + +key_set = { + "offsets", + "path", + "scales", + "image", + "original_size", + "annotations", + "image_id", + "context", +} +anno_key_set = { + "keypoints", + "keypoints_unique", + "with_center_keypoints", + "area", + "boxes", + "is_crowd", + "labels", + "individual_ids", +} + + +@pytest.mark.parametrize("batch_size", [1, 2, random.randint(2, 20)]) +def test_iter_all_dataset_no_transform(batch_size): + if batch_size > 1: # if batched, all images need to be the same size + transform = A.Compose( + [A.Resize(512, 512)], + keypoint_params=A.KeypointParams(format="xy"), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + else: + transform = A.Compose( + [A.Normalize()], + keypoint_params=A.KeypointParams(format="xy"), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + dataset = _get_openfield_dataset(transform=transform) + dataloader = DataLoader(dataset, batch_size=batch_size) + max_num_animals = dataset.parameters.max_num_animals + 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}" + ) + + anno = item["annotations"] + 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)" + ) + + 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)" + ) + + +def _generate_random_test_values_aug(min_exa): + batch_size = random.randint(1, 20) + x_size = random.randint(50, 600) + y_size = random.randint(50, 600) + exaggeration = random.randint(min_exa, 99) + + return batch_size, x_size, y_size, exaggeration + + +@pytest.mark.parametrize( + "batch_size, x_size, y_size, exaggeration", + [ + (1, 512, 512, 1), + _generate_random_test_values_aug(1), + _generate_random_test_values_aug(50), + ], +) +def test_iter_all_augmented_dataset(batch_size, x_size, y_size, exaggeration): + transform = A.Compose( + [ + A.Affine( + scale=(1 - exaggeration * 0.01, 1 + exaggeration), + rotate=(-exaggeration * 2, exaggeration * 2), + translate_px=(-exaggeration * 10, exaggeration * 10), + ), + A.Resize(y_size, x_size), + ], + keypoint_params=A.KeypointParams(format="xy", remove_invisible=False), + bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), + ) + dataset = _get_openfield_dataset(transform=transform) + dataloader = DataLoader(dataset, batch_size=batch_size) + max_num_animals = dataset.parameters.max_num_animals + 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}" + ) + + anno = item["annotations"] + 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)" + ) + + 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() diff --git a/tests/pose_estimation_pytorch/other/test_gaussian_targets.py b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py new file mode 100644 index 0000000000..1c202b1902 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py @@ -0,0 +1,56 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import HeatmapGaussianGenerator + + +@pytest.mark.parametrize( + "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): + # generate annotations + labels = { + "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 + prediction = { + "heatmap": torch.rand((batch_size, num_keypoints, *image_size[:2])), + "locref": torch.rand((batch_size, 2 * num_keypoints, *image_size[:2])), + } + + # generate heatmap + output = HeatmapGaussianGenerator( + num_heatmaps=num_keypoints, + pos_dist_thresh=17, + locref_std=5.0, + ) + output = output(stride, prediction, labels)["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(output, dim=2) + + # get unraveled coords + x = gaus_max % image_size[1] + y = gaus_max // image_size[1] + + # get heatmap center tensor + predict_kp = torch.stack((x, y), dim=-1) + # Remove num_animals dimension - only one animal is supported + labels["keypoints"] = torch.squeeze(labels["keypoints"], dim=1) + + # compare heatmap center to annotation + assert torch.eq(labels["keypoints"], predict_kp).all().item() diff --git a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py new file mode 100644 index 0000000000..b44dc3f39f --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py @@ -0,0 +1,204 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + + +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import HeatmapPlateauGenerator + + +def get_target( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + locref_std: float, + pos_dist_thresh: int, +): + """Summary Getting the target generator for certain annotations, predictions and + image size. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_std (float): scaling factor + pos_dist_thresh (int): radius plateau on the heatmap + + Returns: + target_output (dict): containing the heatmaps, locref_maps and locref_masks. + annotations (dict): containing input keypoint annotations. + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + output: + """ + labels = { + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_joints, 2)) + } # 2 for x,y coords + stride = 1 + prediction = { + "heatmap": torch.rand((batch_size, num_joints, image_size[0], image_size[1])), + "locref": torch.rand((batch_size, 2 * num_joints, image_size[0], image_size[1])), + } + generator = HeatmapPlateauGenerator( + num_heatmaps=num_joints, + pos_dist_thresh=pos_dist_thresh, + locref_std=locref_std, + generate_locref=True, + ) + + targets_output = generator(stride, prediction, labels) + return targets_output, labels + + +data = [(1, 1, 10, (256, 256), 7.2801, 17)] + + +@pytest.mark.parametrize( + "batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh", + data, +) +def test_expected_output( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + locref_stdev: float, + pos_dist_thresh: int, +): + """Summary: + Testing if plateau targets return the expected output. We take a target generator from + get_target function. Given a sequence of random numbers for batch_size, num_animals etc., we assert if + it returns the expected heatmaps and locrefmaps, as well as checking if the output has the expected shape. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_stdev (float): scaling factor + pos_dist_thresh (int): radius plateau on heatmap + + Returns: + None + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + """ + targets_output, annotations = get_target( + batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh + ) + + assert "heatmap" in targets_output + assert "locref" in targets_output + assert targets_output["heatmap"]["target"].shape == ( + batch_size, + num_joints, + image_size[0], + image_size[1], + ) # heatmaps score output + assert targets_output["locref"]["weights"].shape == ( + batch_size, + num_joints * 2, + image_size[0], + image_size[1], + ) + assert targets_output["locref"]["target"].shape == ( + batch_size, + num_joints * 2, + image_size[0], + image_size[1], + ) + + +data = [(1, 1, 10, (256, 256), 7.2801, 17)] + + +@pytest.mark.parametrize( + "batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh", + data, +) +def test_single_animal( + batch_size: int, + num_animals: int, + num_joints: int, + image_size: tuple[int, int], + 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. + + '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 + indices of the first maximal value are returned. From this tensor we exctact x,y coords + and then concatenate these new tensors along a new dimension. Then, we assert if the distance between + each x,y element in annotations and predicted keypoints is smaller or equal to the 'pos_dist_thresh', + which represents the radius of the plateau heatmap. + + Args: + batch_size (int): number of images + num_animals (int): number of animals + num_joints (int): number of bodyparts + image_size (tuple): image size in pixels + locref_stdev (float): scaling factor + pos_dist_thresh (int): radius plateau on heatmap + + Returns: + None + + Examples: + input: + batch_size = 1 + num_animals = 1 + num_joints = 6 + image_size = (256,256) + locref_stdev = 7.2801 + pos_dist_thresh = 17 + """ + targets_output, annotations = get_target( + batch_size, num_animals, num_joints, image_size, locref_stdev, pos_dist_thresh + ) + + targets_output = torch.tensor( + targets_output["heatmap"]["target"].reshape(1, 10, image_size[0] * image_size[1]) + ) # converting from dict to tensor. 'argmax' works on tensors. + + plt_max = torch.argmax(targets_output, dim=2) + # get unraveled coords + x = plt_max % image_size[1] + y = plt_max // image_size[1] + + predict_kp = torch.stack((x, y), dim=-1) + + predict_kp = predict_kp.float() + + annotations["keypoints"] = torch.squeeze(annotations["keypoints"], dim=1) + annotations["keypoints"] = annotations["keypoints"].float() + + dist = torch.norm(annotations["keypoints"] - predict_kp, p=2, dim=-1) + assert (dist <= pos_dist_thresh).all() diff --git a/tests/pose_estimation_pytorch/other/test_helper.py b/tests/pose_estimation_pytorch/other/test_helper.py new file mode 100644 index 0000000000..1dfa250109 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_helper.py @@ -0,0 +1,21 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import torch + + +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 + to_valid_mode = tmp_model.eval + to_valid_mode() + assert not tmp_model.training 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 new file mode 100644 index 0000000000..1ee4073fc8 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py @@ -0,0 +1,132 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.post_processing import ( + match_predictions_to_gt as deeplabcut_torch_match_predictions_gt, +) + + +@pytest.fixture +def animals_and_keypoints_invalid(): + """Summary: + Fixture with invalid pred_kpts and gt_kpts shapes that will raise ValueErrors. + + Returns: + tuple containing: + predicted keypoints(pred_kpts), of shape num_animals, num_keypoints, (x,y,score) + ground truth keypoints (gt_kpts), of shape num_animals, num_keypoints, (x,y) + individual names (indv_names) + """ + gt_kpts = 2 * np.ones((6, 6, 3)) # num animals, num keypoints, (x,y,vis) + gt_kpts[:, :, :2] = np.random.rand(6, 6, 2) + pred_kpts = np.random.rand(6, 8, 3) # num animals, num keypoints, (x,y,score) + indv_names = ["indv1", "indv2"] + return pred_kpts, gt_kpts, indv_names + + +@pytest.fixture +def animals_and_keypoints(): + """Summary: + Fixture with pred_kpts, gt_kpts shapes and indv_names. + + Returns: + tuple containing: + predicted keypoints(pred_kpts), of shape num_animals, num_keypoints, (x,y,score) + ground truth keypoints (gt_kpts), of shape num_animals, num_keypoints, (x,y) + individual names (indv_names) + """ + gt_kpts = 2 * np.ones((6, 6, 3)) # num animals, num keypoints, (x,y,vis) + gt_kpts[:, :, :2] = np.random.rand(6, 6, 2) + + # adding score value because the shape of pred_kpts should be (6,6,3) + score = np.full((gt_kpts.shape[0], gt_kpts.shape[1], 1), 0.5) + pred_kpts = np.concatenate((gt_kpts, score), axis=2) + np.random.shuffle(pred_kpts) # shuffle predicted keypoints + + indv_names = ["indv1", "indv2"] + return pred_kpts, gt_kpts, indv_names + + +def test_invalid_rmse(animals_and_keypoints_invalid: tuple) -> None: + """Summary: + Tets if an invalid output really returns a ValueError in the rmse function. + + Args: + 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 + + with pytest.raises(ValueError): + deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + + +def test_invalid_oks(animals_and_keypoints_invalid: tuple) -> None: + """Summary: + 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) + and individual names (indv_names) + """ + 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) + + +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. + + Args: + animals_and_keypoints (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 + + 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: + """Summary: + Test if oks_match_predictions_to_gt function returns the expected shape output. + + Args: + animals_and_keypoints (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 + + 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_extend_col_ind(animals_and_keypoints: tuple, num_animals: int = 6) -> None: + """Summary: + Test if the column indices have the expected shape. + + Args: + animals_and_keypoints (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 + + 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 new file mode 100644 index 0000000000..0df42715be --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_modelzoo.py @@ -0,0 +1,50 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import os + +import pytest + +from deeplabcut.modelzoo.video_inference import video_inference_superanimal +from deeplabcut.utils import auxiliaryfunctions + +examples_folder = os.path.join( + auxiliaryfunctions.get_deeplabcut_path(), + "modelzoo", + "examples", +) + + +# requires videos to be in the examples folder +@pytest.mark.skip(reason="This behaviour is not implemented yet") +@pytest.mark.parametrize( + "video_paths, superanimal_name", + [ + (f"{examples_folder}/black_dog.mp4", "superanimal_quadruped"), + (f"{examples_folder}/black_dog.mp4", "superanimal_quadruped_hrnetw32"), + (f"{examples_folder}/swear_mouse_tiny.mp4", "superanimal_topviewmouse"), + ( + f"{examples_folder}/swear_mouse_tiny.mp4", + "superanimal_topviewmouse_hrnetw32", + ), + ], +) +def test_video_inference_saves_file(video_paths, superanimal_name): + video_inference_superanimal( + video_paths, + superanimal_name=superanimal_name, + ) + if isinstance(video_paths, str): + video_paths = [video_paths] + for video_path in video_paths: + 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_paf_targets.py b/tests/pose_estimation_pytorch/other/test_paf_targets.py new file mode 100644 index 0000000000..9865cf732c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_paf_targets.py @@ -0,0 +1,37 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import pafs_targets + + +@pytest.mark.parametrize( + "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): + labels = { + "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 = { + "heatmap": torch.rand((batch_size, num_keypoints, image_size[0], image_size[1])), + "paf": torch.rand((batch_size, len(graph) * 2, image_size[0], image_size[1])), + } + generator = pafs_targets.PartAffinityFieldGenerator(graph=graph, width=20) + targets_output = generator(1, prediction, labels) + assert targets_output["paf"]["target"].shape == ( + batch_size, + len(graph) * 2, + image_size[0], + image_size[1], + ) diff --git a/tests/pose_estimation_pytorch/other/test_pose_model.py b/tests/pose_estimation_pytorch/other/test_pose_model.py new file mode 100644 index 0000000000..de36678c8c --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_pose_model.py @@ -0,0 +1,300 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import copy +import random + +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.models as dlc_models +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 + +backbones_dicts = [ + { + "type": "HRNet", + "model_name": "hrnet_w32", + "output_channels": 480, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w18", + "output_channels": 270, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w48", + "output_channels": 720, + "stride": 4, + "interpolate_branches": True, + }, + { + "type": "HRNet", + "model_name": "hrnet_w32", + "output_channels": 32, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + { + "type": "HRNet", + "model_name": "hrnet_w18", + "output_channels": 18, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + { + "type": "HRNet", + "model_name": "hrnet_w48", + "output_channels": 48, + "interpolate_branches": False, + "increased_channel_count": False, + "stride": 4, + }, + {"type": "ResNet", "model_name": "resnet50_gn", "output_channels": 2048, "stride": 32}, +] + +heads_dicts = [ + { + "type": "HeatmapHead", + "predictor": { + "type": "HeatmapPredictor", + "location_refinement": True, + "locref_std": 7.2801, + }, + "target_generator": { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": "num_bodyparts", + "pos_dist_thresh": 17, + "heatmap_mode": "KEYPOINT", + "generate_locref": True, + "locref_std": 7.2801, + }, + "criterion": { + "heatmap": { + "type": "WeightedBCECriterion", + "weight": 1.0, + }, + "locref": { + "type": "WeightedHuberCriterion", + "weight": 0.05, + }, + }, + "heatmap_config": { + "channels": [2048, 1024, -1], + "kernel_size": [2, 2], + "strides": [2, 2], + }, + "locref_config": { + "channels": [2048, 1024, -1], + "kernel_size": [2, 2], + "strides": [2, 2], + }, + "output_channels": -1, + "input_channels": 2048, + "total_stride": 4, + }, + { + "type": "TransformerHead", + "predictor": { + "type": "HeatmapPredictor", + "location_refinement": False, + }, + "target_generator": { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": "num_bodyparts", + "pos_dist_thresh": 17, + "heatmap_mode": "KEYPOINT", + "generate_locref": False, + }, + "criterion": {"type": "WeightedBCECriterion"}, + "dim": 192, + "hidden_heatmap_dim": 384, + "heatmap_dim": -1, + "apply_multi": True, + "heatmap_size": [-1, -1], + "apply_init": True, + "total_stride": 1, + "input_channels": -1, + "output_channels": -1, + "head_stride": 1, + }, + { + "type": "DEKRHead", + "predictor": { + "type": "DEKRPredictor", + "num_animals": 1, + "keypoint_score_type": "heatmap", + "max_absorb_distance": 75, + }, + "target_generator": { + "type": "DEKRGenerator", + "num_joints": "num_bodyparts", + "pos_dist_thresh": 17, + "bg_weight": 0.1, + }, + "criterion": { + "heatmap": { + "type": "WeightedBCECriterion", + "weight": 1.0, + }, + "offset": { + "type": "WeightedHuberCriterion", + "weight": 0.03, + }, + }, + "heatmap_config": { + "channels": [480, 64, -1], + "num_blocks": 1, + "dilation_rate": 1, + "final_conv_kernel": 1, + "block": BasicBlock, + }, + "offset_config": { + "channels": [480, -1, -1], + "num_offset_per_kpt": 15, + "num_blocks": 1, + "dilation_rate": 1, + "final_conv_kernel": 1, + "block": AdaptBlock, + }, + "total_stride": 1, + "input_channels": 480, + "output_channels": -1, + }, +] + + +def _generate_random_backbone_inputs(i): + # Returns sizes that are divisible by 64to be able to predict consistently output size + # (and be able to do the forward pass of HRNet) + x_size_tmp, y_size_tmp = random.randint(100, 1000), random.randint(100, 1000) + return ( + backbones_dicts[i], + (x_size_tmp - x_size_tmp % 64, y_size_tmp - y_size_tmp % 64), + ) + + +@pytest.mark.parametrize( + "backbone_dict, input_size", + [_generate_random_backbone_inputs(i) for i in range(len(backbones_dicts))], +) +def test_backbone(backbone_dict, input_size): + input_tensor = torch.Tensor(1, 3, input_size[1], input_size[0]) + + stride = backbone_dict.pop("stride") + output_channels = backbone_dict.pop("output_channels") + backbone = dlc_models.BACKBONES.build(backbone_dict) + + features = backbone(input_tensor) + _, c, h, w = features.shape + assert c == output_channels + assert h == input_size[1] // stride + assert w == input_size[0] // stride + + +def _generate_random_head_inputs(i): + # Returns sizes that are divisible by 64to be able to predict consistently output size + # (and be able to do the forward pass of HRNet) + x_size_tmp, y_size_tmp = random.randint(8, 500), random.randint(8, 500) + num_kpts = random.randint(2, 50) + return ( + heads_dicts[i], + (x_size_tmp - x_size_tmp % 4, y_size_tmp - y_size_tmp % 4), + num_kpts, + ) + + +@pytest.mark.parametrize( + "head_dict, input_shape, num_keypoints", + [_generate_random_head_inputs(i) for i in range(len(heads_dicts))], +) +def test_head(head_dict, input_shape, num_keypoints): + w, h = input_shape + head_dict = copy.deepcopy(head_dict) + + head_type = head_dict["type"] + input_channels = head_dict.pop("input_channels") + output_channels = head_dict.pop("output_channels") + total_stride = head_dict.pop("total_stride") + if head_type == "HeatmapHead": + output_channels = num_keypoints + head_dict["heatmap_config"]["channels"][2] = output_channels + head_dict["locref_config"]["channels"][2] = 2 * output_channels + head_dict["target_generator"]["num_heatmaps"] = output_channels + input_tensor = torch.zeros((1, input_channels, h, w)) + + elif head_type == "TransformerHead": + output_channels = num_keypoints + input_channels = num_keypoints + head_dict["heatmap_dim"] = h * w + head_dict["heatmap_size"] = [h, w] + head_dict["target_generator"]["num_heatmaps"] = output_channels + input_tensor = torch.zeros((1, input_channels, head_dict["dim"] * 3)) + + elif head_type == "DEKRHead": + 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"][2] = num_keypoints + input_tensor = torch.zeros((1, input_channels, h, w)) + + if "type" in head_dict["criterion"]: + head_dict["criterion"] = CRITERIONS.build(head_dict["criterion"]) + else: + weights = {} + 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"} + 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["predictor"] = PREDICTORS.build(head_dict["predictor"]) + head = dlc_models.HEADS.build(head_dict) + + output = head(input_tensor)["heatmap"] + _, c_out, h_out, w_out = output.shape + assert (h_out == h * total_stride) and (w_out == w * total_stride) + assert c_out == output_channels + + +def test_msa_hrnet(): + # TODO: build microsoft asia hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + pass + + +def test_msa_tokenpose(): + # TODO: build microsoft asia hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + # cf https://github.com/amathislab/BUCTDdev/blob/main/lib/models/transpose_h.py#L1 + pass + + +def test_msa_hrnetCOAM(): + # TODO: build BUCTD COAM hrnet and check dimension of output + # TODO: check if hyperparameters are loaded correctly (from the config file) + pass + + +# TODO: add other model variants our pipeline can build ;) diff --git a/tests/pose_estimation_pytorch/other/test_seq_targets.py b/tests/pose_estimation_pytorch/other/test_seq_targets.py new file mode 100644 index 0000000000..c2816c8650 --- /dev/null +++ b/tests/pose_estimation_pytorch/other/test_seq_targets.py @@ -0,0 +1,51 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from itertools import combinations + +import torch + +from deeplabcut.pose_estimation_pytorch.models.target_generators import ( + TARGET_GENERATORS, +) + + +def test_sequential_generator(): + batch_size = 4 + image_size = 256, 256 + num_keypoints = 12 + num_animals = 2 + graph = [list(edge) for edge in combinations(range(num_keypoints), 2)] + num_limbs = len(graph) + cfg = { + "type": "SequentialGenerator", + "generators": [ + { + "type": "HeatmapPlateauGenerator", + "num_heatmaps": num_keypoints, + "pos_dist_thresh": 17, + "generate_locref": True, + "locref_std": 7.2801, + }, + {"type": "PartAffinityFieldGenerator", "graph": graph, "width": 20}, + ], + } + gen = TARGET_GENERATORS.build(cfg) + + 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), + "paf": torch.rand(batch_size, num_limbs * 2, 32, 32), + } + out = gen(stride=1, outputs=head_outputs, labels=annotations) + assert all(s in out for s in list(head_outputs)) + for k, v in head_outputs.items(): + assert out[k]["target"].shape == v.shape diff --git a/tests/pose_estimation_pytorch/post_processing/test_identity.py b/tests/pose_estimation_pytorch/post_processing/test_identity.py new file mode 100644 index 0000000000..3f16eade5f --- /dev/null +++ b/tests/pose_estimation_pytorch/post_processing/test_identity.py @@ -0,0 +1,59 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests identity matching.""" + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.post_processing.identity import assign_identity + + +@pytest.mark.parametrize( + "prediction, identity_scores, output_order", + [ + ( + [ + [[0, 0, 1.0], [0, 0, 1.0]], # assembly 1 + [[5, 5, 1.0], [5, 5, 1.0]], # assembly 2 + [[9, 9, 1.0], [9, 9, 1.0]], # assembly 3 + ], + [ # a0 -> idv1, a1 -> idv2, a2 -> idv0 + [[0.1, 0.8, 0.3], [0.1, 0.7, 0.3]], # assembly 1 ID scores + [[0.2, 0.1, 0.6], [0.3, 0.1, 0.5]], # assembly 2 ID scores + [[0.7, 0.1, 0.1], [0.6, 0.2, 0.2]], # assembly 3 ID scores + ], + [2, 0, 1], + ), + ( + [ + [[0, 0, 1.0], [0, 0, 1.0]], # assembly 1 + [[1, 1, 1.0], [5, 5, 1.0]], # assembly 2 + [[0, 0, 1.0], [9, 9, 1.0]], # assembly 3 + ], + [ # a0 -> idv0, a1 -> idv1, a2 -> idv2 + [[0.4, 0.4, 0.3], [0.5, 0.3, 0.3]], # assembly 1 ID scores + [[0.4, 0.4, 0.3], [0.3, 0.5, 0.4]], # assembly 2 ID scores + [[0.2, 0.2, 0.4], [0.2, 0.2, 0.3]], # assembly 3 ID scores + ], + [0, 1, 2], + ), + ], +) +def test_single_identity_assignment(prediction, identity_scores, output_order): + predictions = np.array(prediction) + identity_scores = np.array(identity_scores) + new_order = assign_identity(predictions, identity_scores) + predictions_with_id = predictions[new_order] + + print() + print(predictions.shape) + print(identity_scores.shape) + np.testing.assert_equal(predictions[output_order], predictions_with_id) diff --git a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py new file mode 100644 index 0000000000..48f65fd5e1 --- /dev/null +++ b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py @@ -0,0 +1,108 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests pose NMS.""" + +import numpy as np +import pytest + +import deeplabcut.pose_estimation_pytorch.post_processing.nms as nms + + +@pytest.mark.parametrize( + "poses, score_threshold, expected_kept", + [ + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [True], # a single pose should be kept + ), + ( + [ + [[0.0, np.nan, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [True], # a single pose should be kept + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [False, False], # no valid poses + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + ], + 0.1, + [False, True, False, False], # a single valid pose + ), + ( + [ + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[100.0, 100, 0.89], [110, 110, 0.89], [120, 120, 0.89]], + ], + 0.1, + [True, True], # two valid poses, far apart + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + ], + 0.1, + [False, True, True], # two valid poses, far apart + ), + ( + [ + [[0.0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 0.1, + [False, True, True], # two valid poses, far apart, sorted by score + ), + ( + [ + [[0.0, 0, 0.89], [10, 10, 0.89], [20, 20, 0.89]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 0.1, + [False, True, True], # two valid poses, far apart, sorted by score, one suppressed + ), + ( + [ + [[1.0, 0, 0.89], [11, 10, 0.89], [21, 20, 0.89]], + [[100.0, 100, 0.8], [110, 110, 0.8], [120, 120, 0.8]], + [[0.0, 0, 0.9], [10, 10, 0.9], [20, 20, 0.9]], + ], + 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.""" + kept = nms.nms_oks( + predictions=np.asarray(poses), + oks_threshold=0.9, + oks_sigmas=0.1, + score_threshold=0.1, + ) + assert kept.tolist() == expected_kept diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py new file mode 100644 index 0000000000..ae6de38298 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -0,0 +1,77 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for the bottom-up pytorch runner.""" + +from pathlib import Path +from typing import Any + +import pytest + +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.pose_estimation_pytorch.task import Task + +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): + for k, v in data.items(): + if isinstance(v, dict): + print_dict(v, indent=indent + 2) + else: + 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), + } + if multianimal: + project_cfg["bodyparts"] = "MULTI!" + project_cfg["multianimalbodyparts"] = ["head", "shoulder", "knee", "toe"] + project_cfg["uniquebodyparts"] = [] + project_cfg["individuals"] = ["tom", "jerry"] + else: + project_cfg["bodyparts"] = ["head", "shoulder", "knee", "toe"] + project_cfg["uniquebodyparts"] = [] + project_cfg["individuals"] = ["tom"] + + root_path = Path(__file__).parent.parent + template_path = (root_path / "other/test_configs/pytorch_config.yaml").resolve() + assert template_path.is_file(), f"Template config not found at {template_path}" + + 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 = 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 diff --git a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py new file mode 100644 index 0000000000..7c1ebb162f --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py @@ -0,0 +1,194 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the dynamic cropper.""" + +import numpy as np +import pytest +import torch + +from deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping import ( + DynamicCropper, + TopDownDynamicCropper, +) + + +@pytest.mark.parametrize("dynamic", [(False, 0.5, 10)]) +def test_build_dynamic_cropper(dynamic: tuple[bool, float, int]): + cropper = DynamicCropper.build(*dynamic) + should_be_built, threshold, margin = dynamic + if should_be_built: + assert isinstance(cropper, DynamicCropper) + assert cropper.threshold == threshold + assert cropper.margin == margin + else: + assert cropper is None + + +@pytest.mark.parametrize("batch_size", [0, 2, 8]) +def test_dynamic_fails_with_image_batch(batch_size: int): + cropper = DynamicCropper(threshold=0.6, margin=10) + with pytest.raises(RuntimeError): + cropper.crop(torch.zeros(batch_size, 3, 128, 128)) + + +def test_dynamic_fails_with_variable_frame_size(): + cropper = DynamicCropper(threshold=0.6, margin=10) + cropper.crop(torch.zeros(1, 3, 64, 64)) + with pytest.raises(RuntimeError): + cropper.crop(torch.zeros(1, 3, 128, 128)) + + +def test_dynamic_fails_with_update_before_crop(): + cropper = DynamicCropper(threshold=0.6, margin=10) + with pytest.raises(RuntimeError): + cropper.update(torch.ones(5, 17, 3)) + + +@pytest.mark.parametrize("threshold", [0.25, 0.5, 0.8]) +def test_dynamic_cropper_does_nothing_with_low_quality(threshold: float): + cropper = DynamicCropper(threshold=threshold, margin=10) + image_in = torch.ones((1, 3, 32, 32)) + cropper.crop(image_in) + for i in range(10): + pose = _generate_random_pose( + (32, 64), + min_score=0.0, + max_score=threshold - 0.001, + seed=i, + ) + cropper.update(pose) + image_out = cropper.crop(image_in) + assert torch.equal(image_in, image_out) + + +@pytest.mark.parametrize( + "pose, threshold, margin, expected_crop", + [ + ([[float("nan"), float("nan"), float("nan")]], 0.1, 10, [0, 0, 64, 64]), + ([[float("nan"), 30, 0.0]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.0]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.49]], 0.5, 10, [0, 0, 64, 64]), + ([[20, 30, 0.8]], 0.5, 10, [10, 20, 30, 40]), + ([[20, 30, 0.8], [float("nan"), float("nan"), 0.2]], 0.5, 15, [5, 15, 35, 45]), + ([[20, 30, 0.8], [5, 5, 0.2]], 0.5, 15, [0, 0, 35, 45]), + ([[20, 30, 0.8], [35, 30, 0.79]], 0.8, 5, [15, 25, 40, 35]), + ([[40, 10, 0.2], [35, 15, 0.79]], 0.3, 8, [27, 2, 48, 23]), + ( + [ + [[float("nan"), float("nan"), float("nan")]], + [[float("nan"), float("nan"), float("nan")]], + ], + 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], + ), + ], +) +def test_dynamic_cropper_basic_crop( + 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 + + image_in = torch.zeros((1, 3, 64, 64)) + image_in[:, :, y0:y1, x0:x1] = 1 + expected_image_out = torch.ones((1, 3, crop_h, crop_w)) + + cropper = DynamicCropper(threshold=threshold, margin=margin) + image_out = cropper.crop(image_in) + assert torch.equal(image_out, image_in) + + cropper.update(torch.tensor(pose)) + image_out = cropper.crop(image_in) + assert image_out.shape == expected_image_out.shape + assert torch.equal(image_out, expected_image_out) + + pose_out = torch.tensor(pose) + print("\nPose in") + print(pose_out.numpy()) + pose_out[..., 0] -= x0 + pose_out[..., 1] -= y0 + print("Pose out before update") + print(pose_out.numpy()) + cropper.update(pose_out) + print("Pose out after update") + print(pose_out.numpy()) + np.testing.assert_allclose(pose_out.numpy(), np.array(pose)) + + +@pytest.mark.parametrize("size", [128, 256, 291, 320, 480, 500, 640, 800]) +@pytest.mark.parametrize("n", [1, 2, 3, 4, 5]) +@pytest.mark.parametrize("overlap", [0, 1, 5, 10, 100]) +def test_tddc_array_split(size: int, n: int, overlap: int) -> None: + print("\nTesting TopDownDynamicCropper array split") + print("Size:", size) + print("N:", n) + print("Overlap:", overlap) + sections = TopDownDynamicCropper.split_array(size, n, overlap) + print("Sections:") + for section in sections: + print(f" {section}") + + # check that we have the desired number of sections + assert len(sections) == n + + # check that the sections start at 0 and end at the array size + start, end = sections[0][0], sections[-1][1] + assert start == 0 + assert end == size + + # check all sections have size at least 1 + for start, end in sections: + assert start < end + + # check that all sections have the same size + sizes = [end - start for start, end in sections] + 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:], strict=False): + assert end_1 >= start_2 + assert end_1 - start_2 >= overlap + + # check that the difference between overlaps is at most 1 + # FIXME(niels) - auto-correct the overlap to spread it out more evenly + # if n > 1: + # overlaps = [ + # end_1 - start_2 + # for (start_1, end_1), (start_2, end_2) in zip(sections[:-1], sections[1:]) + # ] + # + # assert max(overlaps) - min(overlaps) <= 1 + + +def _generate_random_pose( + image_shape: tuple[int, int], + min_score: float, + max_score: float, + num_animals: int = 3, + num_keypoints: int = 7, + seed: int = 0, +) -> torch.Tensor: + gen = np.random.default_rng(seed) + pose = gen.random((num_animals, num_keypoints, 3)) + pose[..., 0] *= image_shape[0] + pose[..., 1] *= image_shape[1] + pose[..., 2] = (pose[..., 2] * (max_score - min_score)) + min_score + return torch.from_numpy(pose) 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 new file mode 100644 index 0000000000..4eaa6ad543 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Test script for superanimal_humanbody with torchvision detector.""" + +from deeplabcut.pose_estimation_pytorch.apis.utils import ( + TORCHVISION_DETECTORS, + get_filtered_coco_detector_inference_runner, +) +from deeplabcut.pose_estimation_pytorch.models.detectors.filtered_detector import ( + FilteredDetector, +) +from deeplabcut.pose_estimation_pytorch.modelzoo import load_super_animal_config +from deeplabcut.pose_estimation_pytorch.modelzoo.utils import COCO_PERSON_CATEGORY_ID + + +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", + model_name="rtmpose_x", + detector_name=detector_name, + ) + print("Config loaded successfully!") + + # Test loading the torchvision detector directly + print("\nTesting torchvision detector loading...") + entry = TORCHVISION_DETECTORS[detector_name] + weights = entry["weights"] + coco_detector = entry["fn"](weights=weights, box_score_thresh=0.6) + coco_detector.eval() + print("Torchvision detector loaded successfully!") + + # Test loading the FilteredDetector + person_detector = FilteredDetector(coco_detector, class_id=COCO_PERSON_CATEGORY_ID) + person_detector.eval() + print("Filtered detector loaded successfully!") + + _ = get_filtered_coco_detector_inference_runner( + model_name=detector_name, + category_id=COCO_PERSON_CATEGORY_ID, + batch_size=1, + model_config=superanimal_config, + ) + print("Filtered detector runner created successfully!") + + print("\n✅ All tests passed! The torchvision detector integration is working correctly.") + return True + + +if __name__ == "__main__": + print("Testing superanimal_humanbody with torchvision detector...") + success = test_torchvision_detector() + if success: + 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") diff --git a/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py b/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py new file mode 100644 index 0000000000..116f5a1e6a --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_inference_directml_no_grad.py @@ -0,0 +1,68 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests DLC_DIRECTML_NO_GRAD toggles inference_mode vs no_grad (AMD DirectML).""" + +from __future__ import annotations + +import importlib +import os +from unittest.mock import Mock + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.inference as inference +from deeplabcut.pose_estimation_pytorch.config.inference import InferenceConfig, MultithreadingConfig + + +def _reload_with_env(env_value: str | None): + if env_value is None: + os.environ.pop("DLC_DIRECTML_NO_GRAD", None) + else: + os.environ["DLC_DIRECTML_NO_GRAD"] = env_value + importlib.reload(inference) + + +@pytest.fixture(autouse=True) +def _restore_env(): + yield + _reload_with_env(None) # always restore defaults after each test + + +@pytest.mark.parametrize( + ("env_value", "directml_no_grad"), + [(None, False), ("false", False), ("true", True)], +) +def test_directml_no_grad_env(env_value, directml_no_grad): + """env var sets _directml_no_grad and selects the correct torch grad context.""" + _reload_with_env(env_value) + assert inference._directml_no_grad is directml_no_grad + + class _SniffRunner(inference.InferenceRunner): + def __init__(self): + super().__init__( + model=Mock(), + batch_size=1, + inference_cfg=InferenceConfig( + multithreading=MultithreadingConfig(enabled=False), + ), + ) + self.saw_inference_mode: bool | None = None + + def predict(self, inputs: torch.Tensor, **kwargs): + self.saw_inference_mode = torch.is_inference_mode_enabled() + return [{"mock": {"poses": np.zeros((1,), dtype=np.float32)}}] + + runner = _SniffRunner() + runner.inference([np.zeros((1, 3, 8, 8), dtype=np.float32)]) + + assert runner.saw_inference_mode is not directml_no_grad diff --git a/tests/pose_estimation_pytorch/runners/test_logger.py b/tests/pose_estimation_pytorch/runners/test_logger.py new file mode 100644 index 0000000000..9b27314302 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_logger.py @@ -0,0 +1,102 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests loggers.""" + +from pathlib import Path +from typing import Any + +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.logger as logging + + +class MockImageLogger(logging.ImageLoggerMixin): + """Mock image logger.""" + + def log_images( + self, + inputs: dict[str, Any], + outputs: dict[str, torch.Tensor], + targets: dict[str, dict[str, torch.Tensor]], + step: int, + ) -> None: + pass + + +@pytest.mark.parametrize( + "keypoints", + [ + [ + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + ], + [ + [[float("nan"), float("nan")], [float("nan"), float("nan")]], + ], + [ + [[0.0, 0.0], [1, 1], [2, 2]], + ], + [[[float("nan"), 0.0], [1, 1], [2, 2]]], + [[[-1.0, -1.0], [1, 1], [2, 2]]], + [ + [[-1.0, -1.0], [-1.0, -1.0]], + ], + [ + [[-1.0, -1.0], [-1.0, -1.0]], + [[1.0, 1.0], [1.0, 1.0]], + ], + ], +) +@pytest.mark.parametrize("denormalize", [True, False]) +def test_prepare_image(keypoints: list[list[float]], denormalize: bool) -> None: + image = torch.ones((3, 256, 256)) + keypoints = torch.tensor(keypoints) + + print() + print(f"IMAGE: {image.shape}") + print(f"KEYPOINTS: {keypoints.shape}") + for k in keypoints: + print(k) + print() + print() + + logger = MockImageLogger() + logger._prepare_image( + image=image, + denormalize=denormalize, + keypoints=keypoints, + bboxes=None, + ) + + +def test_csv_logger_resume(tmp_path: Path) -> None: + """Test CSVLogger preserves data when resuming from snapshot.""" + log_file = tmp_path / "learning_stats.csv" + + # Initial training: log some metrics + logger1 = logging.CSVLogger(str(tmp_path), "learning_stats.csv") + logger1.log({"loss": 0.5, "accuracy": 0.8}, step=1) + logger1.log({"loss": 0.4, "accuracy": 0.9}, step=2) + + assert log_file.exists() + assert len(logger1._steps) == 2 + + # Resume training: should load existing data + logger2 = logging.CSVLogger(str(tmp_path), "learning_stats.csv") + assert len(logger2._steps) == 2 + assert logger2._steps == [1, 2] + assert logger2._metric_store[0]["loss"] == 0.5 + assert logger2._metric_store[1]["accuracy"] == 0.9 + + # Log new data: should append, not overwrite + logger2.log({"loss": 0.3, "accuracy": 0.95}, step=3) + assert len(logger2._steps) == 3 + assert logger2._steps == [1, 2, 3] diff --git a/tests/pose_estimation_pytorch/runners/test_runners.py b/tests/pose_estimation_pytorch/runners/test_runners.py new file mode 100644 index 0000000000..c3afa93db5 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners.py @@ -0,0 +1,38 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import pickle +from pathlib import Path +from unittest.mock import Mock + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners as runners + + +@pytest.mark.parametrize("value", [True, False]) +def test_set_load_weights_only(value: bool): + print(f"\nget_load_weights_only: {runners.get_load_weights_only()}") + print(f"setting value to {value}") + runners.set_load_weights_only(value) + print(f"get_load_weights_only: {runners.get_load_weights_only()}\n") + assert runners.get_load_weights_only() == value + + +def test_load_snapshot_weights_only_error(tmpdir_factory): + snapshot_dir = Path(tmpdir_factory.mktemp("snapshot-dir")) + snapshot_path = snapshot_dir / "snapshot.pt" + torch.save(dict(content=np.zeros(10)), str(snapshot_path)) + + runners.set_load_weights_only(False) + with pytest.raises(pickle.UnpicklingError): + 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 new file mode 100644 index 0000000000..f59831ca4e --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners_inference.py @@ -0,0 +1,195 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests inference runners.""" + +from unittest.mock import Mock, patch + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.data.postprocessor as post +import deeplabcut.pose_estimation_pytorch.data.preprocessor as prep +import deeplabcut.pose_estimation_pytorch.runners.inference as inference +from deeplabcut.pose_estimation_pytorch import get_load_weights_only +from deeplabcut.pose_estimation_pytorch.task import Task + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.build_optimizer", Mock()) +@pytest.mark.parametrize("task", [Task.DETECT, Task.TOP_DOWN, Task.BOTTOM_UP]) +@pytest.mark.parametrize("weights_only", [None, True, False]) +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" + inference.build_inference_runner( + task=task, + model=Mock(), + device="cpu", + snapshot_path=snapshot, + load_weights_only=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) + + +class MockInferenceRunner(inference.InferenceRunner): + """Mocks the predict function for an inference runner.""" + + def __init__( + self, + batch_size: int = 1, + preprocessor: prep.Preprocessor | None = None, + postprocessor: post.Postprocessor | None = None, + ) -> None: + super().__init__( + model=Mock(), + batch_size=batch_size, + preprocessor=preprocessor, + postprocessor=postprocessor, + ) + self.batch_shapes = [] + + def predict(self, inputs: torch.Tensor) -> list[dict[str, dict[str, np.ndarray]]]: + self.batch_shapes.append(tuple(inputs.shape)) + return [ # return first elem of input + {"mock": {"index": i[0, 0, 0].detach().numpy()}} for i in inputs + ] + + +@pytest.mark.parametrize("batch_size", [1, 2, 4, 8]) +def test_mock_bottom_up(batch_size): + h, w = 640, 480 + images = [i * np.ones((1, 3, h, w)) for i in range(10)] + + runner = MockInferenceRunner(batch_size=batch_size) + predictions = runner.inference(images) + + print() + print(f"Num images: {len(predictions)}") + print(f"Num predictions: {len(predictions)}") + print(f"Batch shapes: {runner.batch_shapes}") + print(80 * "-") + for i in images: + print(i[0, 0, 0, 0]) + print("----") + print(80 * "-") + for p in predictions: + print(p) + print("----") + + _check_batch_shapes(batch_size, h, w, runner.batch_shapes) + assert len(images) == len(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"] + + +@pytest.mark.parametrize("batch_size", [1, 2, 4, 8]) +@pytest.mark.parametrize( + "detections_per_image", + [ + [1, 1, 1, 1, 1], + [0, 1, 0, 1, 1], # some frames might not have predictions + [0, 0, 0, 5, 2], + [1, 2, 3, 4], + [3, 4, 2, 1, 4], + [4, 23, 5, 20, 64, 100], + ], +) +def test_mock_top_down(batch_size, detections_per_image): + h, w = 8, 8 + images = [] + for index, num_detections in enumerate(detections_per_image): + if num_detections == 0: + 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)], + axis=0, + ) + + images.append(detections) + + runner = MockInferenceRunner(batch_size=batch_size) + predictions = runner.inference(images) + + print() + print(f"Num images: {len(predictions)}") + print(f"Num predictions: {len(predictions)}") + print(80 * "-") + for i in images: + for i_det in i: + print(i_det.shape) + print(i_det[0, 0, 0]) + print("----") + + print(80 * "-") + for p in predictions: + print(p) + print("----") + + _check_batch_shapes(batch_size, h, w, runner.batch_shapes) + + assert len(images) == len(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, strict=True): + print(i_det.shape) + print(p_det["mock"]["index"]) + assert i_det[0, 0, 0] == p_det["mock"]["index"] + + +def test_dynamic_pose_inference_calls_dynamic(): + pose_batch = torch.zeros((1, 1, 1, 3)) + pose_batch_updated = torch.ones((1, 1, 1, 3)) + + image_crop = Mock() + image_crop.__len__ = Mock(return_value=1) + + model = Mock() + model.get_predictions = Mock() + model.get_predictions.return_value = dict(bodypart=dict(poses=pose_batch)) + + dynamic = Mock() + dynamic.crop = Mock() + dynamic.crop.return_value = image_crop + dynamic.update = Mock() + dynamic.update.return_value = pose_batch_updated + + runner = inference.PoseInferenceRunner( + model=model, + dynamic=dynamic, + batch_size=1, + ) + image = torch.zeros((1, 3, 64, 64)) + updated_pose = runner.predict(image) + dynamic.crop.assert_called_once_with(image) + dynamic.update.assert_called_once_with(pose_batch) + + assert len(updated_pose) == 1 + np.testing.assert_allclose( + updated_pose[0]["bodypart"]["poses"], + pose_batch_updated[0].cpu().numpy(), + ) + + +def _check_batch_shapes(batch_size, h, w, batch_shapes) -> None: + for b in batch_shapes[:-1]: + assert b[0] == batch_size + assert b[1] == 3 + assert b[2] == h + assert b[3] == w + + assert batch_shapes[-1][0] <= batch_size + assert batch_shapes[-1][1] <= 3 + assert batch_shapes[-1][2] <= h + assert batch_shapes[-1][3] <= w diff --git a/tests/pose_estimation_pytorch/runners/test_runners_train.py b/tests/pose_estimation_pytorch/runners/test_runners_train.py new file mode 100644 index 0000000000..3fa0994217 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_runners_train.py @@ -0,0 +1,320 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from dataclasses import dataclass +from unittest.mock import Mock, patch + +import numpy as np +import pytest +import torch + +import deeplabcut.pose_estimation_pytorch.runners.schedulers as schedulers +import deeplabcut.pose_estimation_pytorch.runners.train as train_runners +from deeplabcut.pose_estimation_pytorch.models import PoseModel +from deeplabcut.pose_estimation_pytorch.models.backbones import ResNet +from deeplabcut.pose_estimation_pytorch.models.heads import HeatmapHead +from deeplabcut.pose_estimation_pytorch.task import Task + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.build_optimizer", Mock()) +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize("task", [Task.DETECT, Task.TOP_DOWN, Task.BOTTOM_UP]) +@pytest.mark.parametrize("weights_only", [True, False]) +def test_load_weights_only_with_build_training_runner(task: Task, weights_only: bool): + runner_config = dict( + optimizer=dict(), + snapshots=dict(max_snapshots=1, save_epochs=5, save_optimizer_state=False), + load_weights_only=weights_only, + ) + with patch("deeplabcut.pose_estimation_pytorch.runners.base.torch.load") as load: + train_runners.build_training_runner( + runner_config=runner_config, + model_folder=Mock(), + task=task, + model=Mock(), + device="cpu", + snapshot_path="snapshot.pt", + ) + load.assert_called_once_with("snapshot.pt", map_location="cpu", weights_only=weights_only) + + +@dataclass +class SchedulerTestConfig: + cfg: dict + init_lr: float + expected_lrs: list[float] + + +TEST_SCHEDULERS = [ + SchedulerTestConfig( + 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], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.1]])), + init_lr=0.1, + expected_lrs=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.5]])), + init_lr=0.1, + expected_lrs=[0.1, 0.5, 0.5, 0.5], + ), + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=3, gamma=0.1)), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 1.0, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01, 0.001], + ), +] + + +@pytest.mark.parametrize("load_head_weights", [True, False]) +def test_load_head_weights(tmp_path_factory, load_head_weights): + model_folder = tmp_path_factory.mktemp("model_folder") + runner_config = dict( + optimizer=dict(type="SGD", params=dict(lr=1)), + snapshots=dict(max_snapshots=1, save_epochs=1, save_optimizer_state=False), + ) + + model = PoseModel( + cfg=dict(), + backbone=ResNet(), + heads=dict( + bodyparts=HeatmapHead( + predictor=Mock(), + target_generator=Mock(), + criterion=Mock(), + aggregator=None, + heatmap_config=dict(channels=[2048, 10], kernel_size=[3], strides=[2]), + ), + ), + ) + + original_state_dict = model.state_dict() + 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) + + with patch("deeplabcut.pose_estimation_pytorch.runners.train.torch.load", load): + r = train_runners.build_training_runner( + runner_config, + model_folder=model_folder, + task=Task.BOTTOM_UP, + model=model, + device="cpu", + snapshot_path=model_folder / "snapshot.pt", + load_head_weights=load_head_weights, + ) + loaded_state_dict = r.model.state_dict() + for k, v in loaded_state_dict.items(): + if load_head_weights or k.startswith("backbone."): + assert torch.equal(v, zero_state_dict[k]) + else: + assert torch.equal(v, original_state_dict[k]) + + +@pytest.mark.parametrize("load_head_weights", [True, False]) +def test_mocked_load_head_weights(tmp_path_factory, load_head_weights): + model_folder = tmp_path_factory.mktemp("model_folder") + snapshot_manager = Mock() + snapshot_manager.model_folder = model_folder + + model = Mock() + model.backbone = Mock() + state_dict = {"backbone.test": 0, "head.test": 1} + state_dict_backbone = {"test": 0} + load = Mock() + load.return_value = dict(model=state_dict) + + with patch("deeplabcut.pose_estimation_pytorch.runners.train.torch.load", load): + _ = train_runners.PoseTrainingRunner( + model=model, + optimizer=Mock(), + snapshot_manager=snapshot_manager, + device="cpu", + snapshot_path="snapshot.pt", + load_head_weights=load_head_weights, + ) + if load_head_weights: + model.load_state_dict.assert_called_once_with(state_dict) + else: + model.backbone.load_state_dict.assert_called_once_with(state_dict_backbone) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_training_with_scheduler(runner_cls, test_cfg: SchedulerTestConfig) -> None: + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + test_cfg.expected_lrs, + ) + assert runner.current_epoch == len(test_cfg.expected_lrs) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_resuming_training_scheduler_every_epoch( + runner_cls, + test_cfg: SchedulerTestConfig, +): + snapshot_to_load = None + for epoch, expected_lr in enumerate(test_cfg.expected_lrs): + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + [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()) + + +@patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) +@pytest.mark.parametrize( + "runner_cls", + [ + train_runners.PoseTrainingRunner, + train_runners.DetectorTrainingRunner, + ], +) +@pytest.mark.parametrize( + "test_cfg, resume_epoch", + [ + ( + SchedulerTestConfig( + 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, 1.0, 1.0, 0.1, 0.1, 0.1], + ), + 3, # cut after the 3rd epoch - restart at LR=1 until epoch 5 + ), + ( + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=4, gamma=0.1)), + init_lr=1.0, + expected_lrs=(4 * [1.0]) + (4 * [0.1]) + (4 * [0.01]) + (4 * [0.001]), + ), + 3, # cut after the 3rd epoch - restart at LR=1 and update at 4 correctly + ), + ( + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=4, gamma=0.1)), + init_lr=1.0, + expected_lrs=(4 * [1.0]) + [0.1, 1, 1, 1] + (4 * [0.1]), + ), + 5, # cut after the 5th epoch - restart at LR=1 and update again at 8 + ), + ], +) +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. + """ + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + test_cfg.expected_lrs[:resume_epoch], + ) + assert runner.current_epoch == resume_epoch + + runner = _fit_runner_and_check_lrs( + runner_cls, + test_cfg.init_lr, + test_cfg.cfg, + expected_lrs=test_cfg.expected_lrs[resume_epoch:], + snapshot_to_load=dict(metadata=dict(epoch=resume_epoch)), + ) + assert runner.current_epoch == len(test_cfg.expected_lrs) + + +def _fit_runner_and_check_lrs( + runner_cls, + init_lr: float, + scheduler_cfg: dict, + expected_lrs: list[float], + snapshot_to_load: dict | None = None, +) -> train_runners.TrainingRunner: + runner_kwargs = dict(device="cpu", eval_interval=1_000_000) + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=init_lr) + scheduler = schedulers.build_scheduler(scheduler_cfg, optimizer) + num_epochs = len(expected_lrs) + + base_path = "deeplabcut.pose_estimation_pytorch.runners" + with patch(f"{base_path}.base.Runner.load_snapshot") as base_mock_load: + with patch(f"{base_path}.train.PoseTrainingRunner.load_snapshot") as mock_load: + snapshot_path = None + base_mock_load.return_value = dict() + mock_load.return_value = dict() + if snapshot_to_load is not None: + snapshot_path = "fake_snapshot.pt" + base_mock_load.return_value = snapshot_to_load + mock_load.return_value = snapshot_to_load + + print() + print(f"Scheduler: {scheduler}") + print(f"Starting training for {num_epochs} epochs") + runner = runner_cls( + model=Mock(), + optimizer=optimizer, + snapshot_manager=Mock(), + scheduler=scheduler, + snapshot_path=snapshot_path, + **runner_kwargs, + ) + + # Mock the step call; check that the learning rate is correct for the epoch + def step(*args, **kwargs): + # the current_epoch value is indexed at 1 + total_epoch = runner.current_epoch - 1 + epoch = total_epoch - runner.starting_epoch + _assert_learning_rates_match(total_epoch, optimizer, expected_lrs[epoch]) + optimizer.step() + return dict(total_loss=0) + + train_loader, val_loader = [Mock()], [Mock()] + runner.step = step + runner.fit(train_loader, val_loader, epochs=num_epochs, display_iters=1000) + + return runner + + +def _assert_learning_rates_match(e, optimizer, expected): + current_lrs = [g["lr"] for g in optimizer.param_groups] + print(f"Epoch {e}: LR={current_lrs}, expected={expected}") + for lr in current_lrs: + assert isinstance(lr, float) + np.testing.assert_almost_equal(lr, expected) diff --git a/tests/pose_estimation_pytorch/runners/test_schedulers.py b/tests/pose_estimation_pytorch/runners/test_schedulers.py new file mode 100644 index 0000000000..af745016a9 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_schedulers.py @@ -0,0 +1,271 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests building schedulers from config.""" + +import random +from dataclasses import dataclass + +import numpy as np +import pytest +import torch +import torch.nn as nn + +import deeplabcut.pose_estimation_pytorch.runners.schedulers as schedulers + + +def generate_random_lr_list(num_floats: int): + """Generate list of lists including random numbers. + + Args: + num_floats: number of floats we want to include in our list + + Returns: + ran_list: random list of sorted numbers, being first number bigger than the last + """ + ran_list = [] + for i in range(num_floats): + random_floats = [random.random()] + ran_list.append(random_floats) + return sorted(ran_list, reverse=True) + + +@pytest.mark.parametrize( + "milestones, lr_list", + [([10, 430], [[0.05], [0.005]]), (list(sorted(random.sample(range(999), 2))), generate_random_lr_list(2))], +) +def test_scheduler(milestones, lr_list): + """Testing schedulers.py. + + Given a list of milestones and a list of learning rates, this function tests + if the length of each list is the same. Furthermore, it will assess if + the current learning rate (output from the function we are testing) is a float + and corresponds to the expected learning rate given the milestones. + + Args: + milestones: list of epochs indices (number of epochs) + lr_list: learning rates list + + Returns: + None + + Examples: + input: + milestones = [10,25,50] + lr_list = [[0.00001],[0.000005],[0.000001]] + """ + + assert len(milestones) == len(lr_list) + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=0.01) + s = schedulers.LRListScheduler(optimizer, milestones=milestones, lr_list=lr_list) + + index_rng = range(milestones[0], milestones[1]) + for i in range((milestones[-1]) + 1): + if i < milestones[0]: + expected_lr = [0.01] + elif i in index_rng: + expected_lr = lr_list[0] + else: + expected_lr = lr_list[1] + + current_lr = s.get_lr()[0] + assert s.get_lr() == expected_lr + assert isinstance(current_lr, float) + optimizer.step() + s.step() + + +@dataclass +class SchedulerTestConfig: + cfg: dict + init_lr: float + expected_lrs: list[float] + + +TEST_SCHEDULERS = [ + SchedulerTestConfig( + 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], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.1]])), + init_lr=0.1, + expected_lrs=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + ), + SchedulerTestConfig( + cfg=dict(type="LRListScheduler", params=dict(milestones=[1], lr_list=[[0.5]])), + init_lr=0.1, + expected_lrs=[0.1, 0.5, 0.5, 0.5], + ), + SchedulerTestConfig( + cfg=dict(type="StepLR", params=dict(step_size=3, gamma=0.1)), + init_lr=1.0, + expected_lrs=[1.0, 1.0, 1.0, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01, 0.001], + ), +] + + +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_build_scheduler(test_cfg: SchedulerTestConfig) -> None: + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + print() + print(f"Scheduler: {s}") + num_epochs = len(test_cfg.expected_lrs) + for e in range(num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + +@pytest.mark.parametrize("test_cfg", TEST_SCHEDULERS) +def test_resume_scheduler_after_each_epoch(test_cfg: SchedulerTestConfig) -> None: + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + print() + print(f"Scheduler: {s}") + num_epochs = len(test_cfg.expected_lrs) + for e in range(num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + new_scheduler = schedulers.build_scheduler(test_cfg.cfg, optimizer) + schedulers.load_scheduler_state(new_scheduler, s.state_dict()) + s = new_scheduler + + +@pytest.mark.parametrize( + "test_cfg, middle_epoch", + [ + (TEST_SCHEDULERS[0], 3), + (TEST_SCHEDULERS[1], 5), + (TEST_SCHEDULERS[2], 2), + (TEST_SCHEDULERS[3], 2), + (TEST_SCHEDULERS[3], 3), + (TEST_SCHEDULERS[3], 4), + ], +) +def test_two_stage_training(test_cfg: SchedulerTestConfig, middle_epoch: int) -> None: + num_epochs = len(test_cfg.expected_lrs) + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + s = schedulers.build_scheduler(test_cfg.cfg, optimizer) + + print() + print(f"Scheduler: {s}") + for e in range(middle_epoch): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + optimizer.step() + s.step() + + optimizer = torch.optim.SGD([torch.randn(2, 2)], lr=test_cfg.init_lr) + new_scheduler = schedulers.build_scheduler(test_cfg.cfg, optimizer) + schedulers.load_scheduler_state(new_scheduler, s.state_dict()) + s = new_scheduler + for e in range(middle_epoch, num_epochs): + _assert_learning_rates_match(e, optimizer, test_cfg.expected_lrs[e]) + s.step() + + +@pytest.mark.parametrize( + "data", + [ + dict( # example with 3 warm-up epochs + config=dict( + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=3), + ), + ), + start_lr=1.0, + expected_lrs=[[0.1], [0.1], [0.1], [1.0], [1.0]], + ), + dict( # example from torch.optim.lr_scheduler.SequentialLR + config=dict( + type="SequentialLR", + params=dict( + schedulers=[ + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=2), + ), + dict(type="ExponentialLR", params=dict(gamma=0.9)), + ], + milestones=[2], + ), + ), + start_lr=1.0, + expected_lrs=[[0.1], [0.1], [1.0], [0.9], [0.81], [0.729]], + ), + dict( # example from torch.optim.lr_scheduler.SequentialLR + config=dict( + type="SequentialLR", + params=dict( + schedulers=[ + dict( + type="ConstantLR", + params=dict(factor=0.1, total_iters=2), + ), + dict(type="StepLR", params=dict(step_size=2, gamma=0.1)), + ], + milestones=[5], + ), + ), + 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 + ], + ), + ], +) +def test_build_sequential_lr(data): + print("\nTESTING") + start_lr = data["start_lr"] + print(f"Start LR: {start_lr}") + model = nn.Linear(in_features=1, out_features=1) + optimizer = torch.optim.SGD(params=model.parameters(), lr=start_lr) + + print("BUILDING") + scheduler = schedulers.build_scheduler(data["config"], optimizer) + + print("RUNNING") + lrs = [] + for epoch in range(len(data["expected_lrs"])): + lrs.append(scheduler.get_last_lr()) + print(scheduler.get_last_lr()) + scheduler.step() + + print(f"Expected: {data['expected_lrs']}") + print(f"Actual: {lrs}") + np.testing.assert_allclose( + np.asarray(data["expected_lrs"]), + np.asarray(lrs), + atol=1e-10, + ) + + +def _assert_learning_rates_match(e, optimizer, expected): + current_lrs = [g["lr"] for g in optimizer.param_groups] + print(f"Epoch {e}: LR={current_lrs}, expected={expected}") + for lr in current_lrs: + assert isinstance(lr, float) + np.testing.assert_almost_equal(lr, expected) diff --git a/tests/pose_estimation_pytorch/runners/test_shelving.py b/tests/pose_estimation_pytorch/runners/test_shelving.py new file mode 100644 index 0000000000..741c3c53c4 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_shelving.py @@ -0,0 +1,160 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for ShelfWriter / ShelfReader.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from deeplabcut.pose_estimation_pytorch.runners.shelving import ( + ShelfReader, + ShelfWriter, +) + +POSE_CFG = { + "all_joints": [[0], [1], [2]], + "all_joints_names": ["snout", "leftear", "rightear"], + "nmsradius": 5, + "minconfidence": 0.1, + "sigma": 1, +} + + +def _make_bodyparts(num_assemblies: int = 2, num_bpts: int = 3) -> np.ndarray: + """(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) + + +# -- lifecycle ---------------------------------------------------------------- + + +def test_write_before_open_raises(tmp_path): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf") + with pytest.raises(ValueError, match="open"): + writer.add_prediction(_make_bodyparts()) + + +def test_open_close_roundtrip(tmp_path): + path = tmp_path / "shelf" + writer = ShelfWriter(POSE_CFG, path) + writer.open() + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(path) + reader.open() + assert "metadata" in reader.keys() + assert "frame00000" in reader.keys() + reader.close() + + +# -- key formatting ----------------------------------------------------------- + + +@pytest.mark.parametrize("num_frames,width", [(9, 1), (100, 2), (1000, 3)]) +def test_key_str_width(tmp_path, num_frames, width): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=num_frames) + writer.open() + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + expected_key = "frame" + "0".zfill(width) + assert expected_key in reader.keys() + reader.close() + + +# -- data shape --------------------------------------------------------------- + + +def test_add_prediction_stores_correct_shapes(tmp_path): + num_assemblies, num_bpts = 2, 3 + bp = _make_bodyparts(num_assemblies, num_bpts) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=10) + writer.open() + writer.add_prediction(bp) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + + coords = data["coordinates"][0] + assert len(coords) == num_bpts + assert coords[0].shape == (num_assemblies, 2) + + scores = data["confidence"] + assert len(scores) == num_bpts + assert scores[0].shape == (num_assemblies, 1) + reader.close() + + +# -- metadata on close -------------------------------------------------------- + + +def test_metadata_nframes_updated_on_close(tmp_path): + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=100) + writer.open() + for _ in range(3): + writer.add_prediction(_make_bodyparts()) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + assert reader["metadata"]["nframes"] == 3 + reader.close() + + +# -- unique bodyparts --------------------------------------------------------- + + +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) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) + writer.open() + writer.add_prediction(bp, unique_bodyparts=ubp) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + assert len(data["coordinates"][0]) == num_bpts + num_unique + assert len(data["confidence"]) == num_bpts + num_unique + reader.close() + + +# -- identity scores ---------------------------------------------------------- + + +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) + + writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) + writer.open() + writer.add_prediction(bp, identity_scores=ids) + writer.close() + + reader = ShelfReader(tmp_path / "shelf") + reader.open() + data = reader["frame0"] + assert "identity" in data + assert len(data["identity"]) == num_bpts + assert data["identity"][0].shape == (num_assemblies, num_individuals) + reader.close() diff --git a/tests/pose_estimation_pytorch/runners/test_task.py b/tests/pose_estimation_pytorch/runners/test_task.py new file mode 100644 index 0000000000..2f821d0aa3 --- /dev/null +++ b/tests/pose_estimation_pytorch/runners/test_task.py @@ -0,0 +1,28 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests the Task enum.""" + +import pytest + +from deeplabcut.pose_estimation_pytorch.task import Task + + +@pytest.mark.parametrize( + "task, task_strings", + [ + (Task.BOTTOM_UP, ["bu", "BU", "bU", "Bu"]), + (Task.TOP_DOWN, ["TD", "tD"]), + (Task.DETECT, ["dt", "DT"]), + ], +) +def test_build_task(task: Task, task_strings: list[str]): + for s in task_strings: + assert task == Task(s) diff --git a/tests/test_auxfun_models.py b/tests/test_auxfun_models.py new file mode 100644 index 0000000000..0684da7651 --- /dev/null +++ b/tests/test_auxfun_models.py @@ -0,0 +1,35 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + + +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from deeplabcut.utils.auxfun_models import MODELTYPE_FILEPATH_MAP, check_for_weights + + +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: + 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) + else: + mocked_download.assert_called_with(modeltype, tmpdir / expected_path) + + def test_bad_modeltype(self): + actual_path = check_for_weights("dummymodel", "nonexistentpath") + self.assertEqual(actual_path, "nonexistentpath") diff --git a/tests/test_auxfun_multianimal.py b/tests/test_auxfun_multianimal.py new file mode 100644 index 0000000000..8e8b7d28dc --- /dev/null +++ b/tests/test_auxfun_multianimal.py @@ -0,0 +1,62 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from itertools import combinations + +import networkx as nx +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.utils import auxfun_multianimal + + +def test_prune_paf_graph(): + n_bpts = 10 # This corresponds to 45 edges + edges = [list(edge) for edge in combinations(range(n_bpts), 2)] + with pytest.raises(ValueError): + pruned_edges = auxfun_multianimal.prune_paf_graph(edges, n_bpts - 2) + pruned_edges = auxfun_multianimal.prune_paf_graph(edges, len(edges)) + + for target in range(20, 45, 5): + pruned_edges = auxfun_multianimal.prune_paf_graph(edges, target) + assert len(pruned_edges) == target + + for degree in (4, 6, 8): + pruned_edges = auxfun_multianimal.prune_paf_graph( + edges, + average_degree=degree, + ) + G = nx.Graph(pruned_edges) + assert np.mean(list(dict(G.degree).values())) == degree + + +def test_reorder_individuals_in_df(): + import random + + # Load sample multi animal data + df = pd.read_hdf("tests/data/montblanc_tracks.h5") + 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 = [individuals[i] for i in permutation_indices] + permutation.append("single") + df_reordered = auxfun_multianimal.reorder_individuals_in_df(df, permutation) + + # Get inverse permutation and reorder the modified data to get back + # to the original + 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) + + # Check + pd.testing.assert_frame_equal(df, df_inverse_reordering) diff --git a/tests/test_auxiliaryfunctions.py b/tests/test_auxiliaryfunctions.py new file mode 100644 index 0000000000..b610cdfbc9 --- /dev/null +++ b/tests/test_auxiliaryfunctions.py @@ -0,0 +1,351 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +from pathlib import Path +from unittest.mock import patch + +import pytest + +from deeplabcut.utils import auxiliaryfunctions +from deeplabcut.utils.auxfun_videos import SUPPORTED_VIDEOS + + +@pytest.mark.parametrize("path_type", [str, Path]) +def test_find_analyzed_data(tmpdir_factory, path_type): + fake_folder = tmpdir_factory.mktemp("videos") + SUPPORTED_VIDEOS = ["avi"] + len(SUPPORTED_VIDEOS) + + SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle1_5" + WRONG_SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle3_5" + + def _create_fake_file(filename): + path = Path(fake_folder) / filename + with open(path, "w") as f: + f.write("") + return path + + for ind, ext in enumerate(SUPPORTED_VIDEOS): + vname = "video" + str(ind) + _ = _create_fake_file(vname + "." + ext) + _ = _create_fake_file(vname + SCORER + ".pickle") + _ = _create_fake_file(vname + SCORER + ".h5") + + # Test for both str and Path types (convert from tmpdir.LocalPath object) + fake_folder = path_type(fake_folder) + for ind, ext in enumerate(SUPPORTED_VIDEOS): + # test if existing models are found: + 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) + + with pytest.raises(FileNotFoundError): + auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), SCORER, filtered=True) + + +@pytest.mark.deprecated +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_get_list_of_videos(tmpdir_factory): + fake_folder = tmpdir_factory.mktemp("videos") + n_ext = len(SUPPORTED_VIDEOS) + + def _create_fake_file(filename): + path = str(fake_folder.join(filename)) + with open(path, "w") as f: + f.write("") + return path + + fake_videos = [] + for ext in SUPPORTED_VIDEOS: + path = _create_fake_file(f"fake.{ext}") + fake_videos.append(path) + + # Add some other office files: + path = _create_fake_file("fake.xls") + path = _create_fake_file("fake.pptx") + + # Add a .pickle and .h5 files + _ = _create_fake_file("fake.pickle") + _ = _create_fake_file("fake.h5") + + # By default, all videos with common extensions are taken from a directory + videos = auxiliaryfunctions.get_list_of_videos( + str(fake_folder), + videotype="", + ) + assert len(videos) == n_ext + + # A list of extensions can also be passed in + videos = auxiliaryfunctions.get_list_of_videos( + str(fake_folder), + videotype=SUPPORTED_VIDEOS, + ) + assert len(videos) == n_ext + + for ext in SUPPORTED_VIDEOS: + videos = auxiliaryfunctions.get_list_of_videos( + str(fake_folder), + videotype=ext, + ) + assert len(videos) == 1 + + videos = auxiliaryfunctions.get_list_of_videos( + str(fake_folder), + videotype="unknown", + ) + assert not len(videos) + + videos = auxiliaryfunctions.get_list_of_videos( + fake_videos, + videotype="", + ) + assert len(videos) == n_ext + + for video in fake_videos: + videos = auxiliaryfunctions.get_list_of_videos([video], videotype="") + assert len(videos) == 1 + + for ext in SUPPORTED_VIDEOS: + videos = auxiliaryfunctions.get_list_of_videos( + fake_videos, + videotype=ext, + ) + assert len(videos) == 1 + + +def test_write_config_has_skeleton(tmpdir_factory): + """Required for backward compatibility.""" + fake_folder = tmpdir_factory.mktemp("fakeConfigs") + fake_config_file = fake_folder / Path("fakeConfig") + auxiliaryfunctions.write_config(fake_config_file, {}) + config_data = auxiliaryfunctions.read_config(fake_config_file) + assert "skeleton" in config_data + + +@pytest.mark.parametrize( + "multianimal, bodyparts, ma_bpts, unique_bpts, comparison_bpts, expected_bpts", + [ + ( + False, + ["head", "shoulders", "knees", "toes"], + None, + None, + {"knees", "others", "toes"}, + ["knees", "toes"], + ), + ( + True, + None, + ["head", "shoulders", "knees"], + ["toes"], + {"knees", "others", "toes"}, + ["knees", "toes"], + ), + ], +) +def test_intersection_of_body_parts_and_ones_given_by_user( + multianimal, bodyparts, ma_bpts, unique_bpts, comparison_bpts, expected_bpts +): + cfg = { + "multianimalproject": multianimal, + "bodyparts": bodyparts, + "multianimalbodyparts": ma_bpts, + "uniquebodyparts": unique_bpts, + } + + if multianimal: + all_bodyparts = list(set(ma_bpts + unique_bpts)) + else: + all_bodyparts = bodyparts + + 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, + ) + print(filtered_bpts) + assert len(expected_bpts) == len(filtered_bpts) + assert all([bpt in expected_bpts for bpt in filtered_bpts]) + + +class MockPath: + def __init__(self, path: Path, st_mtime: int): + self.path = path + self.parent = self.path.parent + self.st_mtime = st_mtime + + def lstat(self): + return self + + +# labeled_folders: (has_H5, H5_st_mtime, folder_name) +@pytest.mark.parametrize( + "labeled_folders, next_folder_name", + [ + ([(True, 1, "a"), (False, None, "b"), (False, None, "c")], "b"), + ([(False, None, "a"), (True, 123, "d"), (False, None, "f")], "f"), + ], +) +def test_find_next_unlabeled_folder( + tmpdir_factory, + monkeypatch, + labeled_folders, + next_folder_name, +): + project_folder = tmpdir_factory.mktemp("project") + fake_cfg = Path(project_folder / "cfg.yaml") + auxiliaryfunctions.write_config(fake_cfg, {"project_path": str(project_folder)}) + + data_folder = project_folder / "labeled-data" + data_folder.mkdir() + rglob_results = [] + for has_h5, h5_last_mod_time, folder_name in labeled_folders: + labeled_folder_path = Path(data_folder / folder_name) + labeled_folder_path.mkdir() + if has_h5: + h5_path = Path(labeled_folder_path / "data.h5") + rglob_results.append(MockPath(h5_path, h5_last_mod_time)) + + def get_rglob_results(*args, **kwargs): + return rglob_results + + monkeypatch.setattr(Path, "rglob", get_rglob_results) + next_folder = auxiliaryfunctions.find_next_unlabeled_folder(fake_cfg) + assert str(next_folder) == str(Path(data_folder / next_folder_name)) + + +@pytest.fixture +def mock_snapshot_folder(tmp_path): + """Mock folder with snapshots.""" + folder = tmp_path / "train" + folder.mkdir() + + # mock files + snapshot_files = [ + "snapshot-4.index", + "snapshot-5.index", + "snapshot-6.index", + "snapshot-3.data-00000-of-00001", + "snapshot-3.index", + "snapshot-3.meta", + ] + for file_name in snapshot_files: + (folder / file_name).touch() + + return folder + + +@pytest.fixture +def mock_no_snapshots_folder(tmp_path): + """Mock folder with no snapshots.""" + folder = tmp_path / "train" + folder.mkdir() + + # mock files + snapshot_files = ["log.txt", "pose_cfg.yaml"] + for file_name in snapshot_files: + (folder / file_name).touch() + + return folder + + +def test_get_snapshots_from_folder(mock_snapshot_folder): + """Test returns expected snapshots in order.""" + snapshot_names = auxiliaryfunctions.get_snapshots_from_folder(mock_snapshot_folder) + assert snapshot_names == ["snapshot-3", "snapshot-4", "snapshot-5", "snapshot-6"] + + +def test_get_snapshots_from_folder_none(mock_no_snapshots_folder): + """Test raises ValueError if no snapshots are found.""" + with pytest.raises(FileNotFoundError): + auxiliaryfunctions.get_snapshots_from_folder(mock_no_snapshots_folder) + + +# --------------------------------------------------------------------------- +# Tests for safe_resolve() and read_config() network-drive path safety +# https://github.com/DeepLabCut/DeepLabCut/issues/3348 +# --------------------------------------------------------------------------- + + +class TestSafeResolve: + """safe_resolve() must return a Path whose str() representation can be + opened by plain string-based I/O — i.e. it must not return Windows 11 SMB + Volume GUID paths like \\\\?\\Volume{...}\\... + """ + + def test_normal_path_is_returned_unchanged(self, tmp_path): + """On a normal local filesystem, safe_resolve returns the resolved path.""" + f = tmp_path / "config.yaml" + f.touch() + result = auxiliaryfunctions.safe_resolve(f) + assert result.exists() + open(result).close() + + def test_fallback_when_resolve_produces_unusable_path(self, tmp_path): + """When resolve() returns a path that cannot be opened as a string, + safe_resolve must fall back to abspath.""" + f = tmp_path / "config.yaml" + f.write_text("project_path: .") + + fake_volume_guid = Path(r"\\?\Volume{DEADBEEF-0000-0000-0000-000000000000}\fake") + + with patch.object(Path, "resolve", return_value=fake_volume_guid): + result = auxiliaryfunctions.safe_resolve(f) + + # Must NOT return the unusable Volume GUID path + assert "Volume{" not in str(result) + # Fallback must be the absolute (non-resolved) path, which exists and is openable + assert result == f.absolute() + open(result).close() + + +class TestReadConfigProjectPath: + """read_config() must never persist a \\\\?\\Volume{GUID}\\... path into + project_path, even on Windows 11 SMB network drives.""" + + def test_project_path_corrected_when_persisted_as_volume_guid(self, tmp_path): + """Regression test for https://github.com/DeepLabCut/DeepLabCut/issues/3348. + + A config.yaml whose project_path was previously corrupted to a + \\\\?\\Volume{GUID}\\... form (e.g. by an older buggy read_config()) + must be corrected to the real directory on the next read. + """ + project_dir = tmp_path / "my_project" + project_dir.mkdir() + config_file = project_dir / "config.yaml" + + bad_path = r"\\?\Volume{DEADBEEF-0000-0000-0000-000000000000}\my_project" + auxiliaryfunctions.write_config(config_file, {"project_path": bad_path}) + + cfg = auxiliaryfunctions.read_config(config_file) + + assert "Volume{" not in str(cfg["project_path"]) + assert cfg["project_path"].exists() + + def test_project_path_updated_when_moved(self, tmp_path): + """read_config() must still update project_path when a project is moved + to a new directory (the original feature that resolve() was meant for).""" + project_dir = tmp_path / "original_location" + project_dir.mkdir() + config_file = project_dir / "config.yaml" + + auxiliaryfunctions.write_config(config_file, {"project_path": "/some/old/path/that/no/longer/exists"}) + + cfg = auxiliaryfunctions.read_config(config_file) + + expected = project_dir.absolute() + assert cfg["project_path"] == expected diff --git a/tests/test_conversioncode.py b/tests/test_conversioncode.py new file mode 100644 index 0000000000..e4074adf62 --- /dev/null +++ b/tests/test_conversioncode.py @@ -0,0 +1,29 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 + +import pandas as pd + +from deeplabcut.utils import conversioncode + + +def test_guarantee_multiindex_rows(test_data_dir): + df_unix = pd.read_hdf(os.path.join(test_data_dir, "trimouse_calib.h5")) + df_posix = df_unix.copy() + df_posix.index = df_posix.index.str.replace("/", "\\") + nrows = len(df_unix) + for df in (df_unix, df_posix): + conversioncode.guarantee_multiindex_rows(df) + assert isinstance(df.index, pd.MultiIndex) + assert len(df) == nrows + assert df.index.nlevels == 3 + assert all(df.index.get_level_values(0) == "labeled-data") + assert all(img.endswith(".png") for img in df.index.get_level_values(2)) diff --git a/tests/test_crossvalutils.py b/tests/test_crossvalutils.py index 7cc67a4f34..5c821bd03e 100644 --- a/tests/test_crossvalutils.py +++ b/tests/test_crossvalutils.py @@ -1,17 +1,28 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 pickle + import numpy as np -from deeplabcut.pose_estimation_tensorflow.lib import crossvalutils +from deeplabcut.core import crossvalutils -BEST_GRAPH = [2, 56, 7, 31, 38, 63, 65, 60, 54, 1, 13] +BEST_GRAPH = [14, 15, 16, 11, 22, 31, 61, 7, 59, 62, 64] +BEST_GRAPH_MONTBLANC = [1, 0, 2, 5, 4, 3] -def test_get_n_best_paf_graphs(uncropped_data_and_metadata): - data, metadata = uncropped_data_and_metadata +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 @@ -19,8 +30,23 @@ def test_get_n_best_paf_graphs(uncropped_data_and_metadata): assert len(paf_inds[-1]) == len(params["paf_graph"]) -def test_benchmark_paf_graphs(uncropped_data_and_metadata): - data, _ = uncropped_data_and_metadata +def test_get_n_best_paf_graphs_montblanc(evaluation_data_and_metadata_montblanc): + data, metadata = evaluation_data_and_metadata_montblanc + params = crossvalutils._set_up_evaluation(data) + paf_inds, dict_ = crossvalutils._get_n_best_paf_graphs( + data, + metadata, + params["paf_graph"], + ) + assert len(paf_inds) == 4 + assert len(dict_) == len(params["paf_graph"]) + assert [len(inds) for inds in paf_inds] == list(range(3, 7)) + assert paf_inds[-1] == BEST_GRAPH_MONTBLANC + assert len(paf_inds[-1]) == len(params["paf_graph"]) + + +def test_benchmark_paf_graphs(evaluation_data_and_metadata): + data, _ = evaluation_data_and_metadata cfg = { "individuals": ["mickey", "minnie", "bianca"], "uniquebodyparts": [], @@ -40,12 +66,56 @@ def test_benchmark_paf_graphs(uncropped_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 miss, purity = results[1].xs("mean", level=1).to_numpy().squeeze() - assert np.isclose(miss, 0.0) - assert np.isclose(purity, 1.0) + assert np.isclose(miss, 0.02, atol=1e-2) + assert np.isclose(purity, 0.98, atol=1e-2) + + +def test_benchmark_paf_graphs_montblanc(evaluation_data_and_metadata_montblanc): + data, metadata = evaluation_data_and_metadata_montblanc + cfg = { + "individuals": [f"bird{i}" for i in range(1, 9)], + "uniquebodyparts": ["center"], + "multianimalbodyparts": [ + "head", + "tail", + "leftwing", + "rightwing", + ], + } + inference_cfg = {"topktoretain": 8, "pcutoff": 0.1, "pafthreshold": 0.1} + results = crossvalutils._benchmark_paf_graphs( + cfg, + inference_cfg, + data, + [BEST_GRAPH_MONTBLANC], + split_inds=[metadata["data"]["trainIndices"], metadata["data"]["testIndices"]], + ) + with open("tests/data/montblanc_map.pickle", "rb") as f: + results_gt = pickle.load(f) + np.testing.assert_equal( + results[1].loc["purity"].to_numpy().squeeze(), + [ + results_gt[0][6][("purity", "mean")], + results_gt[0][6][("purity", "std")], + ], + ) + vals = [ + results[2][0][0]["mAP"], + results[2][0][0]["mAR"], + results[2][0][1]["mAP"], + results[2][0][1]["mAR"], + ] + np.testing.assert_equal( + vals, + [ + results_gt[0][6][("mAP_train", "mean")], + results_gt[0][6][("mAR_train", "mean")], + results_gt[0][6][("mAP_test", "mean")], + results_gt[0][6][("mAR_test", "mean")], + ], + ) diff --git a/tests/test_dataset_augmentation.py b/tests/test_dataset_augmentation.py new file mode 100644 index 0000000000..9e935c7259 --- /dev/null +++ b/tests/test_dataset_augmentation.py @@ -0,0 +1,139 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 imgaug.augmenters as iaa +import numpy as np +import pytest + +from deeplabcut.pose_estimation_tensorflow.datasets import augmentation + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + + +@pytest.mark.parametrize( + "width, height", + [ + (200, 200), + (300, 300), + (400, 400), + ], +) +def test_keypoint_aware_cropping( + sample_image, + sample_keypoints, + width, + height, +): + aug = augmentation.KeypointAwareCropToFixedSize(width=width, height=height) + images_aug, keypoints_aug = aug( + images=[sample_image], + keypoints=[sample_keypoints], + ) + assert len(images_aug) == len(keypoints_aug) == 1 + assert all(im.shape[:2] == (height, width) for im in images_aug) + # Ensure at least a keypoint is visible in each crop + assert all(len(kpts) for kpts in keypoints_aug) + + # Test passing in a batch of frames + n_samples = 8 + images_aug, keypoints_aug = aug( + images=[sample_image] * n_samples, + keypoints=[sample_keypoints] * n_samples, + ) + assert len(images_aug) == len(keypoints_aug) == n_samples + + +@pytest.mark.parametrize( + "width, height", + [ + (200, 200), + (300, 300), + (400, 400), + ], +) +def test_sequential( + sample_image, + sample_keypoints, + width, + height, +): + # Guarantee that images smaller than crop size are handled fine + very_small_image = sample_image[:50, :50] + aug = iaa.Sequential( + [ + iaa.PadToFixedSize(width, height), + augmentation.KeypointAwareCropToFixedSize(width, height), + ] + ) + images_aug, keypoints_aug = aug( + images=[very_small_image], + keypoints=[sample_keypoints], + ) + assert len(images_aug) == len(keypoints_aug) == 1 + assert all(im.shape[:2] == (height, width) for im in images_aug) + # Ensure at least a keypoint is visible in each crop + assert all(len(kpts) for kpts in keypoints_aug) + + # Test passing in a batch of frames + n_samples = 8 + images_aug, keypoints_aug = aug( + images=[very_small_image] * n_samples, + keypoints=[sample_keypoints] * n_samples, + ) + assert len(images_aug) == len(keypoints_aug) == n_samples + + +def test_keypoint_horizontal_flip( + sample_image, + sample_keypoints, +): + keypoints_flipped = sample_keypoints.copy() + keypoints_flipped[:, 0] = sample_image.shape[1] - keypoints_flipped[:, 0] + pairs = [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9), (10, 11)] + aug = augmentation.KeypointFliplr( + keypoints=list(map(str, range(12))), + symmetric_pairs=pairs, + ) + keypoints_aug = aug( + images=[sample_image], + keypoints=[sample_keypoints], + )[1][0] + temp = keypoints_aug.reshape((3, 12, 2)) + for pair in pairs: + temp[:, pair] = temp[:, pair[::-1]] + keypoints_unaug = temp.reshape((-1, 2)) + np.testing.assert_allclose(keypoints_unaug, keypoints_flipped) + + +def test_keypoint_horizontal_flip_with_nans( + sample_image, + sample_keypoints, +): + sample_keypoints[::12] = np.nan + sample_keypoints[2::12] = np.nan + keypoints_flipped = sample_keypoints.copy() + keypoints_flipped[:, 0] = sample_image.shape[1] - keypoints_flipped[:, 0] + pairs = [(0, 1), (2, 3)] + aug = augmentation.KeypointFliplr( + keypoints=list(map(str, range(12))), + symmetric_pairs=pairs, + ) + keypoints_aug = aug( + images=[sample_image], + keypoints=[sample_keypoints], + )[1][0] + temp = keypoints_aug.reshape((3, 12, 2)) + for pair in pairs: + temp[:, pair] = temp[:, pair[::-1]] + keypoints_unaug = temp.reshape((-1, 2)) + np.testing.assert_allclose(keypoints_unaug, keypoints_flipped) diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py new file mode 100644 index 0000000000..69ea13d7c6 --- /dev/null +++ b/tests/test_evaluate.py @@ -0,0 +1,233 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 numpy as np +import pandas as pd +import pytest + +import deeplabcut.pose_estimation_tensorflow as pet +from deeplabcut.pose_estimation_tensorflow.core.evaluate import ( + get_available_requested_snapshots, + get_snapshots_by_index, +) + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + + +def make_single_animal_rmse_df( + bodyparts, + train_indices, + test_indices, + error_data=None, +) -> pd.DataFrame: + if error_data is None: + error_data = np.ones((len(train_indices) + len(test_indices), len(bodyparts))) + return pd.DataFrame(error_data, columns=bodyparts) + + +def make_multi_animal_rmse_df( + scorer, + individuals, + bodyparts, + train_indices, + test_indices, + error_data=None, +) -> pd.DataFrame: + columns = pd.MultiIndex.from_product( + [[scorer], individuals, bodyparts], + names=["scorer", "individuals", "bodyparts"], + ) + if error_data is None: + error_data = np.ones((len(train_indices) + len(test_indices), len(individuals) * len(bodyparts))) + return pd.DataFrame(error_data, columns=columns) + + +KEYPOINT_ERROR_NAMES = [ + "Train error (px)", + "Test error (px)", + "Train error (px) with p-cutoff", + "Test error (px) with p-cutoff", +] + +KEYPOINT_ERROR_TEST_DATA = [ + ( + { + "df_error": make_single_animal_rmse_df( + bodyparts=["leg", "arm", "head"], + train_indices=[0, 1, 3], + test_indices=[2, 4], + ), + "train_indices": [0, 1, 3], + "test_indices": [2, 4], + }, + { + "leg": [1.0, 1.0], # train, test + "arm": [1.0, 1.0], # train, test + "head": [1.0, 1.0], # train, test + }, + ), + ( + { + "df_error": make_single_animal_rmse_df( + bodyparts=["leftHand", "rightHand"], + train_indices=[0, 2], + test_indices=[1, 3], + error_data=[ + [1.0, np.nan], + [1.0, 0.0], + [0.0, 10.0], + [5.0, 5.0], + ], + ), + "train_indices": [0, 2], + "test_indices": [1, 3], + }, + { + "leftHand": [0.5, 3.0], # train, test + "rightHand": [10.0, 2.5], # train, test + }, + ), + ( + { + "df_error": make_single_animal_rmse_df( + bodyparts=["leg", "arm", "head"], + train_indices=[0, 1, 3], + test_indices=[2, 4], + ), + "train_indices": [0, 1, 3], + "test_indices": [2, 4], + }, + { + "leg": [1.0, 1.0], # train, test + "arm": [1.0, 1.0], # train, test + "head": [1.0, 1.0], # train, test + }, + ), + ( + { + "df_error": make_multi_animal_rmse_df( + scorer="john", + individuals=["individual_1", "individual_2"], + bodyparts=["leftArm", "rightArm"], + train_indices=[0, 1, 3], + test_indices=[2], + error_data=[ + # individual_1, individual2 + # leftArm, rightArm, leftArm, rightArm + [1.0, np.nan, 1.0, 2.0], + [2.0, 0.0, 1.0, np.nan], + [3.0, 10.0, 1.0, np.nan], + [10.0, 4.0, np.nan, np.nan], + ], + ), + "train_indices": [0, 1, 3], + "test_indices": [2], + }, + { + "leftArm": [3.0, 2.0], # train, test + "rightArm": [2.0, 10.0], # train, test + }, + ), +] + + +@pytest.mark.parametrize("inputs, expected_values", KEYPOINT_ERROR_TEST_DATA) +def test_evaluate_keypoint_error(inputs, expected_values): + keypoint_error = pet.keypoint_error( + inputs["df_error"], + inputs["df_error"], + inputs["train_indices"], + inputs["test_indices"], + ) + print(inputs["df_error"]) + print(keypoint_error) + for bodypart, mean_errors in expected_values.items(): + for error_name in KEYPOINT_ERROR_NAMES: + if "train" in error_name.lower(): + mean_error = mean_errors[0] + else: + mean_error = mean_errors[1] + + assert keypoint_error.loc[error_name, bodypart] == mean_error + + +def test_get_available_requested_snapshots_ok(): + """Test that the correct snapshots are returned.""" + available = ["snapshot-1", "snapshot-2"] + requested = ["snapshot-2", "snapshot-3"] + + snapshots = get_available_requested_snapshots( + requested_snapshots=requested, + available_snapshots=available, + ) + assert snapshots == ["snapshot-2"] + + +def test_get_available_requested_snapshots_error(): + """Test that a ValueError is raised when requested snapshots are not available.""" + with pytest.raises(ValueError): + get_available_requested_snapshots( + requested_snapshots=["snapshot-2"], + available_snapshots=["snapshot-1", "snapshot-3"], + ) + + +def test_get_snapshots_by_index_int_ok(): + """Test that the correct snapshots are returned.""" + available = ["snapshot-1", "snapshot-2", "snapshot-3"] + + # positive int + snapshots = get_snapshots_by_index( + idx=2, + available_snapshots=available, + ) + assert snapshots == ["snapshot-3"] + + # negative int + snapshots = get_snapshots_by_index( + idx=-2, + available_snapshots=available, + ) + assert snapshots == ["snapshot-2"] + + # all snapshots + snapshots = get_snapshots_by_index( + idx="all", + available_snapshots=available, + ) + assert snapshots == ["snapshot-1", "snapshot-2", "snapshot-3"] + + +def test_get_snapshots_by_index_error(): + """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 + with pytest.raises(IndexError): + get_snapshots_by_index( + idx=5, + available_snapshots=available, + ) + # negative int + with pytest.raises(IndexError): + get_snapshots_by_index( + idx=-4, + available_snapshots=available, + ) + # invalid str + with pytest.raises(IndexError): + get_snapshots_by_index( + idx="1", + available_snapshots=available, + ) diff --git a/tests/test_frame_selection_tools.py b/tests/test_frame_selection_tools.py new file mode 100644 index 0000000000..17b615ad61 --- /dev/null +++ b/tests/test_frame_selection_tools.py @@ -0,0 +1,86 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for frame selection tools.""" + +import math +from unittest.mock import Mock + +import pytest + +import deeplabcut.utils.frameselectiontools as fst + + +@pytest.mark.parametrize( + "fps, duration, n_to_pick, start, end, index", + [ + (32, 10, 10, 0, 1, None), + (16, 100, 50, 0, 1, list(range(100, 500, 5))), + (16, 100, 5, 0.25, 0.3, list(range(100, 500, 5))), + ], +) +def test_uniform_frames(fps, duration, n_to_pick, start, end, index): + start_idx = int(math.floor(start * duration * fps)) + end_idx = int(math.ceil(end * duration * fps)) + if index is None: + valid_indices = list(range(start_idx, end_idx)) + else: + valid_indices = [idx for idx in index if start_idx <= idx <= end_idx] + + clip = Mock() + clip.fps = fps + clip.duration = duration + frames = fst.UniformFrames(clip, n_to_pick, start, end, index) + print(f"FPS: {fps}") + print(f"Duration: {duration}") + print(f"Selected Frames: {frames}") + print(f"Valid Indices: {valid_indices}") + + # Check that we get the number of frames we asked for + assert len(frames) == n_to_pick, f"Wrong nb. of frames: {n_to_pick}!={len(frames)}" + # Check that all indices are valid + for index in frames: + assert index in valid_indices, f"Invalid index: {index} not in {valid_indices}" + # Check that all frames are unique + assert len(set(frames)) == len(frames), "Duplicate indices found" + + +@pytest.mark.parametrize( + "fps, nframes, n_to_pick, start, end, index", + [ + (32, 320, 10, 0, 1, None), + (16, 1600, 50, 0, 1, list(range(100, 500, 5))), + (16, 1600, 5, 0.25, 0.3, list(range(100, 500, 5))), + ], +) +def test_uniform_frames_cv2(fps, nframes, n_to_pick, start, end, index): + start_idx = int(math.floor(start * nframes)) + end_idx = int(math.ceil(end * nframes)) + if index is None: + valid_indices = list(range(start_idx, end_idx)) + else: + valid_indices = [idx for idx in index if start_idx <= idx <= end_idx] + + cap = Mock() + cap.fps = fps + cap.__len__ = Mock(return_value=nframes) + frames = fst.UniformFramescv2(cap, n_to_pick, start, end, index) + print(f"FPS: {fps}") + print(f"Nframes: {nframes}") + print(f"Selected Frames: {frames}") + print(f"Valid Indices: {valid_indices}") + + # Check that we get the number of frames we asked for + assert len(frames) == n_to_pick, f"Wrong nb. of frames: {n_to_pick}!={len(frames)}" + # Check that all indices are valid + for index in frames: + assert index in valid_indices, f"Invalid index: {index} not in {valid_indices}" + # Check that all frames are unique + assert len(set(frames)) == len(frames), "Duplicate indices found" diff --git a/tests/test_inferenceutils.py b/tests/test_inferenceutils.py index a968a26fa2..d91fb6c4a1 100644 --- a/tests/test_inferenceutils.py +++ b/tests/test_inferenceutils.py @@ -1,12 +1,22 @@ -import numpy as np +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 import pickle +from copy import deepcopy + +import numpy as np import pytest -from deeplabcut.pose_estimation_tensorflow.lib import inferenceutils from scipy.spatial.distance import squareform - -TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") +from deeplabcut.core import inferenceutils def test_conv_square_to_condensed_indices(): @@ -16,7 +26,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) @@ -27,9 +37,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 @@ -37,28 +45,42 @@ def test_calc_object_keypoint_similarity(real_assemblies): assert inferenceutils.calc_object_keypoint_similarity(xy3, xy1, sigma) == 0 assert np.isnan(inferenceutils.calc_object_keypoint_similarity(xy1, xy3, sigma)) + # Test flipped keypoints + xy4 = xy1.copy() + 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 + def test_match_assemblies(real_assemblies): assemblies = real_assemblies[0] - matched, unmatched = inferenceutils.match_assemblies( - assemblies, assemblies[::-1], 0.01 - ) - assert not unmatched - for ass1, ass2, oks in matched: - assert ass1 is ass2 - assert oks == 1 + 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 + assert m.oks == 1 - matched, unmatched = inferenceutils.match_assemblies([], assemblies, 0.01) - assert not matched - assert all(ass1 is ass2 for ass1, ass2 in zip(unmatched, assemblies)) + num_gt, matches = inferenceutils.match_assemblies([], assemblies, 0.01) + assert len(matches) == 0 + assert num_gt == len(assemblies) 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) + assert dict_["mAP"] == dict_["mAR"] == 1 + assert len(dict_["precisions"]) == len(dict_["recalls"]) == n_thresholds + assert dict_["precisions"].shape[1] == 101 + np.testing.assert_allclose(dict_["precisions"], 1) + dict_ = inferenceutils.evaluate_assembly( - assemblies, assemblies, oks_thresholds=thresholds + assemblies, + assemblies, + oks_thresholds=thresholds, + symmetric_kpts=[(0, 5), (1, 4)], ) assert dict_["mAP"] == dict_["mAR"] == 1 assert len(dict_["precisions"]) == len(dict_["recalls"]) == n_thresholds @@ -75,7 +97,7 @@ def test_link(): j1 = inferenceutils.Joint(pos1, conf, idx=idx1) j2 = inferenceutils.Joint(pos2, conf, idx=idx2) link = inferenceutils.Link(j1, j2) - assert link.confidence == conf ** 2 + assert link.confidence == conf**2 assert link.idx == (idx1, idx2) assert link.to_vector() == [*pos1, *pos2] @@ -92,7 +114,8 @@ def test_assembly(): assert ass.data[j2.label, -1] == -1 assert ass.area == 0 assert ass.intersection_with(ass) == 1.0 - assert np.all(np.isnan(ass._dict["data"])) + # Original (cached) coordinates must have remained empty + assert np.all(np.isnan(ass._dict["data"][:, :2])) ass.remove_joint(j2) assert len(ass) == 1 @@ -108,8 +131,8 @@ def test_assembly(): assert len(ass3) == 2 -def test_assembler(tmpdir_factory, real_assemblies): - with open(os.path.join(TEST_DATA_DIR, "trimouse_full.pickle"), "rb") as file: +def test_assembler(tmpdir_factory, real_assemblies, test_data_dir): + with open(os.path.join(test_data_dir, "trimouse_full.pickle"), "rb") as file: data = pickle.load(file) with pytest.warns(UserWarning): ass = inferenceutils.Assembler( @@ -135,26 +158,126 @@ def test_assembler(tmpdir_factory, real_assemblies): [3, 4], [0, 2], ] - paf_inds = [ass.graph.index(edge) for edge in naive_graph] - ass.graph = naive_graph - ass.paf_inds = paf_inds + ass.paf_inds = [ass.graph.index(edge) for edge in naive_graph] 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")) + ass.to_pickle(output_dir.join("fake.pickle")) + + +def test_assembler_with_single_bodypart(real_assemblies, test_data_dir): + with open(os.path.join(test_data_dir, "trimouse_full.pickle"), "rb") as file: + temp = pickle.load(file) + data = {"metadata": temp.pop("metadata")} + for k, dict_ in temp.items(): + data[k] = { + "coordinates": (dict_["coordinates"][0][:1],), + "confidence": dict_["confidence"][:1], + } + ass = inferenceutils.Assembler( + data, + max_n_individuals=3, + n_multibodyparts=1, + ) + ass.metadata["joint_names"] = ass.metadata["joint_names"][:1] + ass.metadata["num_joints"] = 1 + ass.metadata["paf_graph"] = [] + ass.metadata["paf"] = [] + ass.metadata["bpts"] = [0] + ass.metadata["ibpts"] = [0] + ass.assemble(chunk_size=0) + assert not ass.unique + assert len(ass.assemblies) == len(real_assemblies) + assert all(len(a) == 3 for a in ass.assemblies.values()) + + +def test_assembler_with_unique_bodypart(real_assemblies_montblanc, test_data_dir): + with open(os.path.join(test_data_dir, "montblanc_full.pickle"), "rb") as file: + data = pickle.load(file) + ass = inferenceutils.Assembler( + data, + max_n_individuals=3, + n_multibodyparts=4, + pcutoff=0.1, + min_affinity=0.1, + ) + assert len(ass.metadata["imnames"]) == 180 + assert ass.n_keypoints == 5 + assert len(ass.graph) == len(ass.paf_inds) == 6 + 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_gt = np.concatenate( + [ass.xy for assemblies in real_assemblies_montblanc[0].values() for ass in assemblies] ) + np.testing.assert_equal(assemblies, assemblies_gt) + + +def test_assembler_with_identity(tmpdir_factory, real_assemblies, test_data_dir): + with open(os.path.join(test_data_dir, "trimouse_full.pickle"), "rb") as file: + data = pickle.load(file) + + # Generate fake identity predictions + for k, v in data.items(): + if k != "metadata": + conf = v["confidence"] + ids = [np.random.rand(c.shape[0], 3) for c in conf] + v["identity"] = ids - output_name = tmpdir_factory.mktemp("data").join("fake.h5") - ass.to_h5(output_name) - ass.to_pickle(str(output_name).replace("h5", "pickle")) + ass = inferenceutils.Assembler(data, max_n_individuals=3, n_multibodyparts=12) + assert ass._has_identity + assert len(ass.metadata["imnames"]) == 50 + assert ass.n_keypoints == 12 + assert len(ass.graph) == len(ass.paf_inds) == 66 + # Assemble based on the smallest graph to speed up testing + naive_graph = [ + [0, 1], + [7, 8], + [6, 7], + [10, 11], + [4, 5], + [5, 6], + [8, 9], + [9, 10], + [0, 3], + [3, 4], + [0, 2], + ] + ass.paf_inds = [ass.graph.index(edge) for edge in naive_graph] + 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 all(np.all(_.data[:, -1] != -1) for a in ass.assemblies.values() for _ in a) + + # Test now with identity only and ensure assemblies + # contain only parts of a single group ID. + ass.identity_only = True + ass.assemble() + assert len(ass.assemblies) == len(real_assemblies) + eq = [] + for a in ass.assemblies.values(): + for _ in a: + ids = _.data[:, -1] + ids = ids[~np.isnan(ids)] + eq.append(np.all(ids == ids[0])) + assert all(eq) + + output_dir = tmpdir_factory.mktemp("data") + ass.to_h5(output_dir.join("fake.h5")) + ass.to_pickle(output_dir.join("fake.pickle")) -def test_assembler_calibration(real_assemblies): - with open(os.path.join(TEST_DATA_DIR, "trimouse_full.pickle"), "rb") as file: +def test_assembler_calibration(real_assemblies, test_data_dir): + with open(os.path.join(test_data_dir, "trimouse_full.pickle"), "rb") as file: data = pickle.load(file) ass = inferenceutils.Assembler(data, max_n_individuals=3, n_multibodyparts=12) - ass.calibrate(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")) + ass.calibrate(os.path.join(test_data_dir, "trimouse_calib.h5")) assert ass._kde is not None assert ass.safe_edge @@ -167,7 +290,14 @@ def test_assembler_calibration(real_assemblies): j2 = inferenceutils.Joint(tuple(assembly.xy[1]), label=1) link = inferenceutils.Link(j1, j2) p = ass.calc_link_probability(link) - assert np.isclose(p, 0.990, atol=1e-3) + assert np.isclose(p, 0.993, atol=1e-3) + + # Test empty assembly + assembly_ = deepcopy(assembly) + assembly_.data[:, :2] = np.nan + mahal, proba = ass.calc_assembly_mahalanobis_dist(assembly_, return_proba=True) + assert np.isinf(mahal) + assert proba == 0 def test_find_outlier_assemblies(real_assemblies): diff --git a/tests/test_pose_multianimal_imgaug.py b/tests/test_pose_multianimal_imgaug.py new file mode 100644 index 0000000000..bc4c4f8899 --- /dev/null +++ b/tests/test_pose_multianimal_imgaug.py @@ -0,0 +1,122 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 + +import numpy as np +import pytest + +from deeplabcut.core.config import read_config_as_dict, write_config +from deeplabcut.pose_estimation_tensorflow.datasets import ( + Batch, + PoseDatasetFactory, + pose_multianimal_imgaug, +) + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + + +def mock_imread(path, mode): + return (np.random.rand(400, 400, 3) * 255).astype(np.uint8) + + +pose_multianimal_imgaug.imread = mock_imread + + +@pytest.fixture() +def ma_dataset(test_data_dir): + ## TODO @deruyter92 2026-06-15: this test config is currently invalid and needs to be + # updated. For now it is updated in place. (see https://github.com/DeepLabCut/UnitTestData/issues/4) + for cfg_name in ("config.yaml", "pose_cfg.yaml"): + cfg_path = os.path.join(test_data_dir, cfg_name) + cfg = read_config_as_dict(cfg_path) + if len(cfg.get("bodyparts", [])) > 0 and len(cfg.get("multianimalbodyparts", [])) > 0: + cfg["bodyparts"] = "MULTI!" + write_config(cfg_path, cfg, overwrite=True) + + cfg = read_config_as_dict(os.path.join(test_data_dir, "pose_cfg.yaml")) + cfg["project_path"] = test_data_dir + cfg["dataset"] = "trimouse_train_data.pickle" + return PoseDatasetFactory.create(cfg) + + +@pytest.mark.parametrize( + "scale, stride", + [ + (0.6, 2), + (0.6, 4), + (0.6, 8), + (0.8, 4), + (1.0, 8), + (1.2, 8), + (0.6, 4), + (0.8, 8), + ], +) +def test_calc_target_and_scoremap_sizes( + ma_dataset, + scale, + stride, +): + ma_dataset.cfg["global_scale"] = scale + ma_dataset.cfg["stride"] = stride + # Disable stochastic scale jitter + ma_dataset.cfg["scale_jitter_lo"] = 1 + ma_dataset.cfg["scale_jitter_up"] = 1 + target_size, sm_size = ma_dataset.calc_target_and_scoremap_sizes() + np.testing.assert_equal(np.asarray([400, 400]) * scale, target_size) + np.testing.assert_equal(target_size / stride, sm_size) + + +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, 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, strict=False): + inds = id_ + start + mask_ = mask[inds] + np.testing.assert_equal(joints[:, 0], id_[mask_]) + np.testing.assert_equal(joints[:, 1:], batch_joint[inds][mask_]) + start += id_.size + + +def test_build_augmentation_pipeline(ma_dataset): + for prob in (0.3, 0.5): + _ = ma_dataset.build_augmentation_pipeline(prob) + + +@pytest.mark.parametrize("num_idchannel", range(4)) +def test_get_targetmaps(ma_dataset, num_idchannel): + ma_dataset.cfg["num_idchannel"] = num_idchannel + batch = ma_dataset.get_batch()[1:] + target_size, sm_size = ma_dataset.calc_target_and_scoremap_sizes() + 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.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[2] == 2 * ma_dataset.cfg["num_limbs"] + + +def test_batching(ma_dataset): + for _ in range(10): + ma_dataset.next_batch() diff --git a/tests/test_predict_multianimal.py b/tests/test_predict_multianimal.py new file mode 100644 index 0000000000..4646a6ca93 --- /dev/null +++ b/tests/test_predict_multianimal.py @@ -0,0 +1,96 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 numpy as np +import pytest + +from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + +RADIUS = 5 +THRESHOLD = 0.01 +STRIDE = 8 + + +def test_extract_detections(model_outputs, ground_truth_detections): + scmaps, locrefs, _ = model_outputs + inds_gt = [] + for i in range(scmaps.shape[3]): + scmap = scmaps[0, ..., i] + peaks = predict_multianimal.find_local_maxima(scmap, RADIUS, THRESHOLD) + inds_gt.append(np.c_[peaks, np.ones(len(peaks)).reshape((-1, 1)) * i]) + inds_gt = np.concatenate(inds_gt).astype(int) + pos_gt = np.concatenate(ground_truth_detections[0]["coordinates"][0]) + prob_gt = np.concatenate(ground_truth_detections[0]["confidence"]) + inds = predict_multianimal.find_local_peak_indices_maxpool_nms( + scmaps, + RADIUS, + THRESHOLD, + ) + with tf.compat.v1.Session() as sess: + inds = sess.run(inds) + pos = predict_multianimal.calc_peak_locations(locrefs, inds, STRIDE) + s, r, c, b = inds.T + prob = scmaps[s, r, c, b].reshape((-1, 1)) + idx = np.argsort(inds[:, -1], kind="mergesort") + np.testing.assert_equal(inds[idx, 1:], inds_gt) + np.testing.assert_almost_equal(pos[idx], pos_gt, decimal=3) + np.testing.assert_almost_equal(prob[idx], prob_gt, decimal=5) + + +def test_association_costs(model_outputs, ground_truth_detections): + costs_gt = ground_truth_detections[0]["costs"] + peak_inds = predict_multianimal.find_local_peak_indices_maxpool_nms( + model_outputs[0], + RADIUS, + THRESHOLD, + ) + with tf.compat.v1.Session() as sess: + peak_inds = sess.run(peak_inds) + graph = [[i, j] for i in range(12) for j in range(i + 1, 12)] + preds = predict_multianimal.compute_peaks_and_costs( + *model_outputs, + peak_inds, + graph=graph, + paf_inds=np.arange(len(graph)), + n_id_channels=0, + stride=STRIDE, + )[0] + assert all(k in preds for k in ("coordinates", "confidence", "costs")) + 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() + ] + 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()) + + +def test_compute_peaks_and_costs_no_graph(model_outputs): + peak_inds = predict_multianimal.find_local_peak_indices_maxpool_nms( + model_outputs[0], + RADIUS, + THRESHOLD, + ) + with tf.compat.v1.Session() as sess: + peak_inds = sess.run(peak_inds) + preds = predict_multianimal.compute_peaks_and_costs( + *model_outputs, + peak_inds, + graph=[], + paf_inds=[], + n_id_channels=0, + stride=STRIDE, + )[0] + assert "costs" not in preds diff --git a/tests/test_predict_supermodel.py b/tests/test_predict_supermodel.py new file mode 100644 index 0000000000..1453984620 --- /dev/null +++ b/tests/test_predict_supermodel.py @@ -0,0 +1,48 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 numpy as np +import pytest + +from deeplabcut.pose_estimation_tensorflow.modelzoo.api import superanimal_inference + + +def test_get_multi_scale_frames(): + fake_img = (255 * np.random.rand(600, 800, 3)).astype(np.uint8) + ar = fake_img.shape[1] / fake_img.shape[0] + heights = list(range(100, 1000, 100)) + frames, shapes = superanimal_inference.get_multi_scale_frames( + fake_img, + heights, + ) + assert len(frames) == len(shapes) == len(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) + + +@pytest.mark.parametrize("scale", [0.7, 1.5, 2]) +def test_project_pred_to_original_size(scale): + old_shape = 400, 600, 3 + new_shape = old_shape[0] // scale, old_shape[1] // scale, 3 + xs = [10, 25, 50, 100] + conf = [[1] for _ in range(len(xs))] + coords = [[np.array([[x, x]]) for x in xs]] + preds = { + "coordinates": coords, + "confidence": conf, + } + preds_orig = superanimal_inference._project_pred_to_original_size( + preds, + old_shape, + new_shape, + ) + 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, strict=False)]) diff --git a/tests/test_refine_train_dataset/test_outlierframes.py b/tests/test_refine_train_dataset/test_outlierframes.py new file mode 100644 index 0000000000..a0d5f229ba --- /dev/null +++ b/tests/test_refine_train_dataset/test_outlierframes.py @@ -0,0 +1,255 @@ +from unittest.mock import MagicMock + +import numpy as np +import pandas as pd +import pytest + +from deeplabcut.refine_training_dataset import outlier_frames + +# ---------------------------- +# Helpers / fixtures +# ---------------------------- + +STATS = [ + "distance", + "sig", + "meanx", + "meany", + "lowerCIx", + "higherCIx", + "lowerCIy", + "higherCIy", +] + + +@pytest.fixture +def patch_hdf_write(monkeypatch): + """ + Avoid filesystem / pytables dependency when storeoutput='full' is used. + Also lets us assert that the write path is still exercised. + """ + mock = MagicMock() + monkeypatch.setattr(pd.DataFrame, "to_hdf", mock) + return mock + + +@pytest.fixture +def patch_fit_sarimax(monkeypatch): + def fake_fit_sarimax_model(x, p, p_bound, alpha, ARdegree, MAdegree): + x = np.asarray(x, dtype=float) + mean = x.copy() + ci = np.c_[mean - 1.0, mean + 1.0] + return mean, ci + + mock = MagicMock(side_effect=fake_fit_sarimax_model) + monkeypatch.setattr(outlier_frames, "FitSARIMAXModel", mock) + return mock + + +@pytest.fixture +def sparse_multianimal_df(): + """ + maDLC-like sparse layout: + - 2 individuals with shared bodyparts + - unique bodyparts present only under a special 'single' bucket + This breaks if reconstructed with the full Cartesian product of the non-'coords' levels, + e.g. multi-animal projects with unique bodyparts were previously + producing many extra columns for the non-existent combinations of individual x unique bodypart. + """ + n_frames = 7 + scorer = "DLC_scorer" + individuals = ["ind1", "ind2"] + shared_bodyparts = [f"shared_{i}" for i in range(18)] + unique_bodyparts = [f"unique_{i}" for i in range(4)] + coords = ["x", "y", "likelihood"] + + tuples = [] + + # Shared bodyparts for each real individual + for ind in individuals: + for bp in shared_bodyparts: + for c in coords: + tuples.append((scorer, ind, bp, c)) + + # Unique bodyparts only under a special bucket + for bp in unique_bodyparts: + for c in coords: + tuples.append((scorer, "single", bp, c)) + + columns = pd.MultiIndex.from_tuples(tuples, names=["scorer", "individuals", "bodyparts", "coords"]) + + # 18 shared * 2 + 4 unique = 40 streams, each with x/y/likelihood + assert len(columns) == 40 * 3 + + rng = np.random.default_rng(42) + values = rng.normal(size=(n_frames, len(columns))) + + # Keep likelihood valid / boring + likelihood_mask = columns.get_level_values("coords") == "likelihood" + values[:, likelihood_mask] = 0.9 + + df = pd.DataFrame(values, columns=columns) + return df + + +@pytest.fixture +def dense_multianimal_df(): + """ + Dense/full-combination layout: + every individual x bodypart combination exists. + For this topology, the old from_product(...) logic and the new "preserve + actual tuples" logic should produce the same output columns (assuming the + dataframe is created in canonical product order, which we do here). + """ + n_frames = 5 + scorer = "DLC_scorer" + individuals = ["ind1", "ind2"] + bodyparts = ["nose", "tail", "paw"] + coords = ["x", "y", "likelihood"] + + tuples = [(scorer, ind, bp, c) for ind in individuals for bp in bodyparts for c in coords] + + columns = pd.MultiIndex.from_tuples(tuples, names=["scorer", "individuals", "bodyparts", "coords"]) + + rng = np.random.default_rng(42) + values = rng.normal(size=(n_frames, len(columns))) + likelihood_mask = columns.get_level_values("coords") == "likelihood" + values[:, likelihood_mask] = 0.95 + + df = pd.DataFrame(values, columns=columns) + return df + + +def _expected_output_columns_from_actual_streams(df): + """ + Expected output columns preserve actual non-'coords' tuples and append the 8 derived stats. + """ + base_cols = df.xs("x", axis=1, level="coords", drop_level=True).columns + return pd.MultiIndex.from_tuples( + [(tuple(col) if isinstance(col, tuple) else (col,)) + (stat,) for col in base_cols for stat in STATS], + names=df.columns.names, + ) + + +def _expected_output_columns_from_dense_product(df): + """ + Expected output columns for the previous implementation: + full Cartesian product of all non-'coords' levels, then the 8 derived stats. + This is only correct / behavior-preserving for dense layouts. + """ + columns = df.columns + prod = [] + for i in range(columns.nlevels - 1): + prod.append(columns.get_level_values(i).unique()) + prod.append(STATS) + return pd.MultiIndex.from_product(prod, names=columns.names) + + +# ---------------------------- +# Tests +# ---------------------------- + + +def test_compute_deviations_regression_sparse_unique_bodyparts( + sparse_multianimal_df, + patch_fit_sarimax, + patch_hdf_write, +): + """ + Regression test for the following maDLC unique-bodypart bug: + output columns must match the actual sparse stream layout rather than an + inflated Cartesian product of all non-'coords' level values. + """ + df = sparse_multianimal_df + n_frames = len(df) + + d, o, data = outlier_frames.compute_deviations( + df, + dataname="dummy.h5", + p_bound=0.01, + alpha=0.01, + ARdegree=3, + MAdegree=1, + storeoutput="full", + ) + + # There are 40 real streams in the sparse fixture + n_streams = 40 + + # Shape sanity checks + assert d.shape == (n_frames,) + assert o.shape == (n_frames,) + assert data.shape == (n_frames, n_streams * 8) + + # Column layout must preserve only the actual streams + expected_columns = _expected_output_columns_from_actual_streams(df) + assert data.columns.equals(expected_columns) + + # xs(...) on the last level should still work exactly as before + distance = data.xs("distance", axis=1, level=-1) + sig = data.xs("sig", axis=1, level=-1) + assert distance.shape == (n_frames, n_streams) + assert sig.shape == (n_frames, n_streams) + + # With the fake fitter, predictions equal observations => zero distances and sig + np.testing.assert_allclose(d, 0.0) + np.testing.assert_allclose(o, 0.0) + + # FitSARIMAXModel should be called twice per stream (x and y) + assert patch_fit_sarimax.call_count == 2 * n_streams + + # "full" path should still try to persist the result + patch_hdf_write.assert_called_once() + + +def test_compute_deviations_behavior_preserved_for_dense_layout( + dense_multianimal_df, + patch_fit_sarimax, + patch_hdf_write, +): + """ + Behavior-preserved check: + for a dense layout where every combination exists, the fixed implementation + should produce the same columns that the old from_product(...) logic would + have produced. + """ + df = dense_multianimal_df + n_frames = len(df) + n_streams = len(df.xs("x", axis=1, level="coords", drop_level=True).columns) + + d, o, data = outlier_frames.compute_deviations( + df, + dataname="dummy.h5", + p_bound=0.01, + alpha=0.01, + ARdegree=3, + MAdegree=1, + storeoutput="full", + ) + + # Basic shape / output checks + assert d.shape == (n_frames,) + assert o.shape == (n_frames,) + assert data.shape == (n_frames, n_streams * 8) + + # For dense data, new behavior should match old dense-product behavior exactly + expected_old_dense_columns = _expected_output_columns_from_dense_product(df) + expected_new_columns = _expected_output_columns_from_actual_streams(df) + + # Sanity check of the fixture assumption: + # in the dense case, these should indeed be identical. + assert expected_new_columns.equals(expected_old_dense_columns) + + # Actual output should match that shared expected index + assert data.columns.equals(expected_old_dense_columns) + + # Still selectable by derived-stat level + assert data.xs("distance", axis=1, level=-1).shape == (n_frames, n_streams) + assert data.xs("sig", axis=1, level=-1).shape == (n_frames, n_streams) + + # Deterministic fake fitter + np.testing.assert_allclose(d, 0.0) + np.testing.assert_allclose(o, 0.0) + + assert patch_fit_sarimax.call_count == 2 * n_streams + patch_hdf_write.assert_called_once() diff --git a/tests/test_stitcher.py b/tests/test_stitcher.py index 8cab81fff2..699ba96d7f 100644 --- a/tests/test_stitcher.py +++ b/tests/test_stitcher.py @@ -1,7 +1,18 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 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 @@ -10,7 +21,6 @@ N_TRACKLETS = 20 -@pytest.fixture() def fake_tracklet(): inds = np.arange(TRACKLET_START, TRACKLET_START + TRACKLET_LEN) data = np.empty((TRACKLET_LEN, N_DETS, 4)) @@ -20,6 +30,12 @@ def fake_tracklet(): return Tracklet(data, inds) +def make_fake_tracklets(): + tracklet = fake_tracklet() + tracklet_single = Tracklet(tracklet.data[:, :1], tracklet.inds) + return tracklet, tracklet_single + + @pytest.fixture() def fake_stitcher(): inds = np.arange(TRACKLET_LEN) @@ -30,94 +46,105 @@ def fake_stitcher(): return TrackletStitcher(tracklets, n_tracks=2) -def test_tracklet_wrong_inputs(fake_tracklet): +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_wrong_inputs(tracklet): with pytest.raises(ValueError): - _ = Tracklet(fake_tracklet.data[..., :2], fake_tracklet.inds) + _ = Tracklet(tracklet.data[..., :2], tracklet.inds) with pytest.raises(ValueError): - _ = Tracklet(fake_tracklet.data[: TRACKLET_LEN - 2], fake_tracklet.inds) + _ = Tracklet(tracklet.data[: TRACKLET_LEN - 2], tracklet.inds) -def test_tracklet_monotonic_indices(fake_tracklet): - tracklet_inv = Tracklet(fake_tracklet.data[::-1], fake_tracklet.inds[::-1]) - np.testing.assert_equal(fake_tracklet.inds, tracklet_inv.inds) - np.testing.assert_equal(fake_tracklet.xy, tracklet_inv.xy) +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_monotonic_indices(tracklet): + tracklet_inv = Tracklet(tracklet.data[::-1], tracklet.inds[::-1]) + np.testing.assert_equal(tracklet.inds, tracklet_inv.inds) + np.testing.assert_equal(tracklet.xy, tracklet_inv.xy) -def test_tracklet(fake_tracklet): - assert len(fake_tracklet) == TRACKLET_LEN - assert fake_tracklet.likelihood == 1 - assert fake_tracklet.identity == TRACKLET_ID - assert fake_tracklet.start == TRACKLET_START - assert fake_tracklet.end == TRACKLET_START + TRACKLET_LEN - 1 +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet(tracklet): + assert len(tracklet) == TRACKLET_LEN + assert tracklet.likelihood == 1 + assert tracklet.identity == TRACKLET_ID + assert tracklet.start == TRACKLET_START + assert tracklet.end == TRACKLET_START + TRACKLET_LEN - 1 np.testing.assert_equal( - fake_tracklet.centroid, np.full((TRACKLET_LEN, 2), np.arange(N_DETS).mean()) + tracklet.centroid, + np.full((TRACKLET_LEN, 2), np.arange(tracklet.data.shape[1]).mean()), ) - fake_tracklet2 = Tracklet(fake_tracklet.data, fake_tracklet.inds + TRACKLET_LEN) - assert fake_tracklet not in fake_tracklet2 - tracklet = fake_tracklet + fake_tracklet2 - tracklet -= fake_tracklet - np.testing.assert_equal(tracklet.data, fake_tracklet2.data) - np.testing.assert_equal(tracklet.inds, fake_tracklet2.inds) - tracklet2 = fake_tracklet + fake_tracklet + tracklet2 = Tracklet(tracklet.data, tracklet.inds + TRACKLET_LEN) + assert tracklet not in tracklet2 + tracklet_new = tracklet + tracklet2 + tracklet_new -= tracklet + np.testing.assert_equal(tracklet_new.data, tracklet2.data) + np.testing.assert_equal(tracklet_new.inds, tracklet2.inds) + tracklet2 = tracklet + tracklet assert tracklet2.contains_duplicates() -def test_tracklet_default_identity(fake_tracklet): - fake_tracklet.data = fake_tracklet.data[..., :3] - assert fake_tracklet.identity == -1 +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_default_identity(tracklet): + tracklet.data = tracklet.data[..., :3] + assert tracklet.identity == -1 -def test_tracklet_data_access(fake_tracklet): - np.testing.assert_equal( - fake_tracklet.get_data_at(TRACKLET_START), fake_tracklet.data[0] - ) - fake_tracklet.set_data_at(TRACKLET_START + 1, fake_tracklet.data[0] * 2) - np.testing.assert_equal(fake_tracklet.data[1], fake_tracklet.data[0] * 2) - fake_tracklet.del_data_at(TRACKLET_START + 1) - assert not fake_tracklet.is_continuous - assert TRACKLET_START + 1 not in fake_tracklet.inds +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_data_access(tracklet): + np.testing.assert_equal(tracklet.get_data_at(TRACKLET_START), tracklet.data[0]) + tracklet.set_data_at(TRACKLET_START + 1, tracklet.data[0] * 2) + np.testing.assert_equal(tracklet.data[1], tracklet.data[0] * 2) + tracklet.del_data_at(TRACKLET_START + 1) + assert not tracklet.is_continuous + assert TRACKLET_START + 1 not in tracklet.inds -@pytest.mark.parametrize("where, norm", [("head", False), ("tail", True)]) -def test_tracklet_calc_velocity(fake_tracklet, where, norm): - _ = fake_tracklet.calc_velocity(where, norm) +@pytest.mark.parametrize( + "tracklet, where, norm", + list(zip(make_fake_tracklets(), ("head", "tail"), (False, True), strict=False)), +) +def test_tracklet_calc_velocity(tracklet, where, norm): + _ = tracklet.calc_velocity(where, norm) -def test_tracklet_calc_rate_of_turn(fake_tracklet): +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_calc_rate_of_turn(tracklet): for where in ("head", "tail"): - _ = fake_tracklet.calc_rate_of_turn(where) + _ = tracklet.calc_rate_of_turn(where) -def test_tracklet_affinities(fake_tracklet): - other_tracklet = Tracklet(fake_tracklet.data, fake_tracklet.inds + TRACKLET_LEN) - _ = fake_tracklet.dynamic_similarity_with(other_tracklet) - _ = fake_tracklet.dynamic_dissimilarity_with(other_tracklet) - _ = fake_tracklet.shape_dissimilarity_with(other_tracklet) - _ = fake_tracklet.box_overlap_with(other_tracklet) - _ = fake_tracklet.motion_affinity_with(other_tracklet) - _ = fake_tracklet.distance_to(other_tracklet) +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_tracklet_affinities(tracklet): + other_tracklet = Tracklet(tracklet.data, tracklet.inds + TRACKLET_LEN) + _ = tracklet.dynamic_similarity_with(other_tracklet) + _ = tracklet.dynamic_dissimilarity_with(other_tracklet) + _ = tracklet.shape_dissimilarity_with(other_tracklet) + _ = tracklet.box_overlap_with(other_tracklet) + _ = tracklet.motion_affinity_with(other_tracklet) + _ = tracklet.distance_to(other_tracklet) -def test_stitcher_wrong_inputs(fake_tracklet): +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_stitcher_wrong_inputs(tracklet): with pytest.raises(IOError): _ = TrackletStitcher([], n_tracks=2) with pytest.raises(ValueError): - _ = TrackletStitcher([fake_tracklet], n_tracks=2, min_length=2) + _ = TrackletStitcher([tracklet], n_tracks=2, min_length=2) -def test_purify_tracklets(fake_tracklet): - fake_tracklet.data = np.full_like(fake_tracklet.data, np.nan) - assert TrackletStitcher.purify_tracklet(fake_tracklet) is None - fake_tracklet.data[0] = 1 - tracklet = TrackletStitcher.purify_tracklet(fake_tracklet) - assert len(tracklet) == 1 - assert tracklet.inds == fake_tracklet.inds[0] +@pytest.mark.parametrize("tracklet", make_fake_tracklets()) +def test_purify_tracklets(tracklet): + tracklet.data = np.full_like(tracklet.data, np.nan) + assert TrackletStitcher.purify_tracklet(tracklet) is None + tracklet.data[0] = 1 + tracklet_pure = TrackletStitcher.purify_tracklet(tracklet) + assert len(tracklet_pure) == 1 + assert tracklet_pure.inds == tracklet.inds[0] def test_stitcher(tmpdir_factory, fake_stitcher): assert len(fake_stitcher) == N_TRACKLETS assert fake_stitcher.n_frames == TRACKLET_LEN - assert fake_stitcher.compute_max_gap() == 1 + assert fake_stitcher.compute_max_gap(fake_stitcher.tracklets) == 1 fake_stitcher.build_graph(max_gap=1) fake_stitcher.stitch(add_back_residuals=True) output_name = tmpdir_factory.mktemp("data").join("fake.h5") @@ -157,8 +184,9 @@ def test_stitcher_real(tmpdir_factory, real_tracklets): stitcher = TrackletStitcher.from_dict_of_dict(real_tracklets, n_tracks=3) assert len(stitcher) == 3 assert all(tracklet.is_continuous for tracklet in stitcher.tracklets) + assert all(tracklet.identity == -1 for tracklet in stitcher.tracklets) assert not stitcher.residuals - assert stitcher.compute_max_gap() == 0 + assert stitcher.compute_max_gap(stitcher.tracklets) == 0 stitcher.build_graph() assert stitcher.G.number_of_edges() == 9 @@ -171,3 +199,65 @@ def test_stitcher_real(tmpdir_factory, real_tracklets): output_name = tmpdir_factory.mktemp("data").join("fake.h5") stitcher.write_tracks(output_name, ["mickey", "minnie", "bianca"]) + + +def test_stitcher_montblanc(real_tracklets_montblanc): + stitcher = TrackletStitcher.from_dict_of_dict( + real_tracklets_montblanc, + n_tracks=3, + ) + assert len(stitcher) == 5 + assert all(tracklet.is_continuous for tracklet in stitcher.tracklets) + assert all(tracklet.identity == -1 for tracklet in stitcher.tracklets) + assert len(stitcher.residuals) == 1 + assert len(stitcher.residuals[0]) == 2 + assert stitcher.compute_max_gap(stitcher.tracklets) == 5 + + stitcher.build_graph() + assert stitcher.G.number_of_edges() == 18 + weights = [w for *_, w in stitcher.G.edges.data("weight") if w] + assert weights == [2453, 24498, 5428] + + stitcher.stitch() + assert len(stitcher.tracks) == 3 + assert all(len(track) >= 176 for track in stitcher.tracks) + assert all(0.996 <= track.likelihood <= 1 for track in stitcher.tracks) + + df_gt = pd.read_hdf("tests/data/montblanc_tracks.h5") + df = stitcher.format_df() + np.testing.assert_equal(df.to_numpy(), df_gt.to_numpy()) + + +def test_stitcher_with_identity(real_tracklets): + # Add fake IDs + for i in range(3): + tracklet = real_tracklets[i] + for v in tracklet.values(): + v[:, -1] = i + stitcher = TrackletStitcher.from_dict_of_dict(real_tracklets, n_tracks=3) + tracklets = sorted(stitcher, key=lambda t: t.identity) + assert all(tracklet.identity == i for i, tracklet in enumerate(tracklets)) + + # Split all tracklets in half + tracklets = [t for track in stitcher for t in stitcher.split_tracklet(track, [25])] + stitcher = TrackletStitcher(tracklets, n_tracks=3) + assert len(stitcher) == 6 + + stitcher.build_graph() + weight = stitcher.G.edges[("0out", "3in")]["weight"] + + def weight_func(t1, t2): + w = 0.01 if t1.identity == t2.identity else 1 + return w * t1.distance_to(t2) + + stitcher.build_graph(weight_func=weight_func) + assert stitcher.G.number_of_edges() == 27 + new_weight = stitcher.G.edges[("0out", "3in")]["weight"] + assert new_weight == weight // 100 + + stitcher.stitch() + assert len(stitcher.tracks) == 3 + assert all(len(track) == 50 for track in stitcher.tracks) + assert all(0.998 <= track.likelihood <= 1 for track in stitcher.tracks) + tracks = sorted(stitcher.tracks, key=lambda t: t.identity) + assert all(track.identity == i for i, track in enumerate(tracks)) diff --git a/tests/test_tf_install_smoke.py b/tests/test_tf_install_smoke.py new file mode 100644 index 0000000000..af19b8c8a3 --- /dev/null +++ b/tests/test_tf_install_smoke.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# + +"""Smoke test for TensorFlow when optional TF extras are installed.""" + +import pytest + +tf = pytest.importorskip( + "tensorflow", + reason="TensorFlow not installed (use a project extra such as .[tf])", +) + + +def test_tensorflow_imports_and_has_matmul() -> None: + assert tf.__version__ + a = tf.constant([[1.0, 2.0]]) + b = tf.constant([[3.0], [4.0]]) + c = tf.matmul(a, b) + + if tf.executing_eagerly(): + result = c.numpy() + else: + with tf.compat.v1.Session() as sess: + result = sess.run(c) + + assert (result == [[11.0]]).all() + + +def test_tf_slim_imports_and_has_conv2d() -> None: + try: + import tf_slim as slim + except ImportError as e: + raise AssertionError("tf_slim is not installed or not importable") from e + + assert slim.conv2d(tf.constant([[[[1.0]]]]), 1, kernel_size=[1, 1], stride=1).shape == (1, 1, 1, 1) + + +def test_tf_keras_imports_and_has_regularizers() -> None: + try: + import tf_keras as keras + except ImportError as e: + raise AssertionError("tf_keras is not installed or not importable") from e + import numpy as np + + assert keras.regularizers.l2(0.01).l2 == np.array(0.01, dtype="float32") diff --git a/tests/test_trackingutils.py b/tests/test_trackingutils.py index 1446d332a6..b3dac6d263 100644 --- a/tests/test_trackingutils.py +++ b/tests/test_trackingutils.py @@ -1,6 +1,17 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 numpy as np import pytest -from deeplabcut.pose_estimation_tensorflow.lib import trackingutils + +from deeplabcut.core import trackingutils @pytest.fixture() @@ -11,21 +22,17 @@ def ellipse(): def test_ellipse(ellipse): assert ellipse.aspect_ratio == 2 - assert ellipse.geometry is not None - 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): - assert ellipse.calc_iou_with(ellipse) == 1 assert ellipse.calc_similarity_with(ellipse) == 1 def test_ellipse_fitter(): fitter = trackingutils.EllipseFitter() assert fitter.fit(np.random.rand(2, 2)) is None - xy = np.asarray([[-2, 0], [2, 0], [0, 1], [0, -1]], dtype=np.float) + xy = np.asarray([[-2, 0], [2, 0], [0, 1], [0, -1]], dtype=float) assert fitter.fit(xy) is not None fitter.sd = 0 el = fitter.fit(xy) @@ -34,9 +41,8 @@ def test_ellipse_fitter(): def test_ellipse_tracker(ellipse): tracker1 = trackingutils.EllipseTracker(ellipse.parameters) - assert tracker1.id == 0 tracker2 = trackingutils.EllipseTracker(ellipse.parameters) - assert tracker2.id == 1 + assert tracker1.id != tracker2.id tracker1.update(ellipse.parameters) assert tracker1.hit_streak == 1 state = tracker1.predict() @@ -47,37 +53,84 @@ def test_ellipse_tracker(ellipse): def test_sort_ellipse(): tracklets = dict() - mot = trackingutils.SORTEllipse(1, 1, 0.6) + mot_tracker = trackingutils.SORTEllipse(1, 1, 0.6) poses = np.random.rand(2, 10, 3) - trackers = mot.track(poses[..., :2]) + trackers = mot_tracker.track(poses[..., :2]) assert trackers.shape == (2, 7) trackingutils.fill_tracklets(tracklets, trackers, poses, imname=0) assert all(id_ in tracklets for id_ in trackers[:, -2]) + assert all(np.array_equal(tracklets[n][0], pose) for n, pose in enumerate(poses)) -def test_tracking(real_assemblies, real_tracklets): +def test_tracking_ellipse(real_assemblies, real_tracklets): tracklets_ref = real_tracklets.copy() _ = tracklets_ref.pop("header", None) tracklets = dict() mot_tracker = trackingutils.SORTEllipse(1, 1, 0.6) for ind, assemblies in real_assemblies.items(): - animals = np.stack([ass.data[:, :3] for ass in assemblies]) + animals = np.stack([ass.data for ass in assemblies]) + 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()) + + +def test_box_tracker(): + bbox = 0, 0, 100, 100 + tracker1 = trackingutils.BoxTracker(bbox) + tracker2 = trackingutils.BoxTracker(bbox) + assert tracker1.id != tracker2.id + tracker1.update(bbox) + assert tracker1.hit_streak == 1 + state = tracker1.predict() + np.testing.assert_equal(bbox, state) + _ = tracker1.predict() + assert tracker1.hit_streak == 0 + + +def test_tracking_box(real_assemblies, real_tracklets): + tracklets_ref = real_tracklets.copy() + _ = tracklets_ref.pop("header", None) + tracklets = dict() + mot_tracker = trackingutils.SORTBox(1, 1, 0.1) + for ind, assemblies in real_assemblies.items(): + animals = np.stack([ass.data for ass in assemblies]) + bboxes = trackingutils.calc_bboxes_from_keypoints(animals) + 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()) + + +def test_tracking_montblanc( + real_assemblies_montblanc, + real_tracklets_montblanc, +): + tracklets_ref = real_tracklets_montblanc.copy() + _ = tracklets_ref.pop("header", None) + tracklets = dict() + tracklets["single"] = real_assemblies_montblanc[1] + mot_tracker = trackingutils.SORTEllipse(1, 1, 0.6) + for ind, assemblies in real_assemblies_montblanc[0].items(): + animals = np.stack([ass.data for ass in assemblies]) 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(): + frame = f"frame{str(ind).zfill(3)}" if k != "single" else ind + np.testing.assert_equal(data, ref[frame]) 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 @@ -92,9 +145,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 new file mode 100644 index 0000000000..c93445f19b --- /dev/null +++ b/tests/test_trainingsetmanipulation.py @@ -0,0 +1,229 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 + +import numpy as np +import pandas as pd +import pytest +from skimage import color, io + +from deeplabcut.generate_training_dataset import ( + SplitTrials, + format_multianimal_training_data, + 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 + + +def test_read_image_shape_fast(tmp_path, test_data_dir): + path_rgb_image = os.path.join(test_data_dir, "image.png") + img = imread(path_rgb_image, mode="skimage") + shape = img.shape + assert read_image_shape_fast(path_rgb_image) == (shape[2], shape[0], shape[1]) + path_gray_image = str(tmp_path / "gray.png") + io.imsave(path_gray_image, color.rgb2gray(img).astype(np.uint8)) + assert read_image_shape_fast(path_gray_image) == (1, shape[0], shape[1]) + + +def test_split_trials(): + n_rows = 123 + train_fractions = np.arange(50, 96) / 100 + for frac in train_fractions: + train_inds, test_inds = SplitTrials( + range(n_rows), + frac, + enforce_train_fraction=True, + ) + assert (len(train_inds) / (len(train_inds) + len(test_inds))) == frac + train_inds = train_inds[train_inds != -1] + test_inds = test_inds[test_inds != -1] + assert (len(train_inds) + len(test_inds)) == n_rows + + +def test_format_training_data(monkeypatch, test_data_dir): + fake_shape = 3, 480, 640 + monkeypatch.setattr( + trainingsetmanipulation, + "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) + 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(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) + + +def test_format_multianimal_training_data(monkeypatch, test_data_dir): + fake_shape = 3, 480, 640 + monkeypatch.setattr( + multiple_individuals_trainingsetmanipulation, + "read_image_shape_fast", + lambda _: fake_shape, + ) + df = pd.read_hdf(os.path.join(test_data_dir, "trimouse_calib.h5")) + guarantee_multiindex_rows(df) + train_inds = list(range(10)) + n_decimals = 1 + data = format_multianimal_training_data(df, train_inds, "", n_decimals) + assert len(data) == len(train_inds) + 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()) + + +@pytest.mark.parametrize( + "videos, expected_filenames", + [ + ([], []), + (["/data/my-video.mov"], ["my-video"]), + (["/data/my-video.mp4", "/data2/my-video.mov"], ["my-video"]), + (["/data/my-video.mov", "/data/video2.mov"], ["my-video", "video2"]), + (["/a/v1.mov", "/a/v2.mp4", "/b/v1.mov"], ["v1", "v2"]), + (["v1.mov", "v2.mov", "v1.mov"], ["v1", "v2"]), + (["/a/v1.mp4", "/a/v2.mov", "/b/v2.mov"], ["v1", "v2"]), + (["/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]): + filenames = parse_video_filenames(videos) + assert filenames == expected_filenames + + +def test_format_training_data_ignores_likelihood_columns(monkeypatch, test_data_dir): + fake_shape = 3, 480, 640 + monkeypatch.setattr( + trainingsetmanipulation, + "read_image_shape_fast", + lambda _: fake_shape, + ) + + # Base single-animal dataframe (x/y only) + df = pd.read_hdf(os.path.join(test_data_dir, "trimouse_calib.h5")).xs( + "mus1", + level="individuals", + axis=1, + ) + guarantee_multiindex_rows(df) + + # Add a likelihood column so the layout becomes: + # x, y, likelihood, x, y, likelihood, ... + new_cols = [] + new_arrays = [] + + coord_level = df.columns.names.index("coords") + + for col in df.columns: + new_cols.append(col) + new_arrays.append(df[col].to_numpy()) + + if col[coord_level] == "y": + lik_col = list(col) + lik_col[coord_level] = "likelihood" + new_cols.append(tuple(lik_col)) + new_arrays.append(np.ones(len(df), dtype=float)) + + df_with_likelihood = pd.DataFrame( + np.column_stack(new_arrays), + index=df.index, + columns=pd.MultiIndex.from_tuples(new_cols, names=df.columns.names), + ) + + train_inds = list(range(10)) + + baseline_train_data, baseline_matlab_data = format_training_data(df, train_inds, 12, "") + train_data, matlab_data = format_training_data(df_with_likelihood, train_inds, 12, "") + + # The presence of likelihood columns should not change the formatted result + assert len(train_data) == len(baseline_train_data) + assert len(matlab_data) == len(baseline_matlab_data) + + for got, expected in zip(train_data, baseline_train_data, strict=False): + assert got["image"] == expected["image"] + assert got["size"] == expected["size"] + assert np.array_equal(got["joints"], expected["joints"]) + + for got, expected in zip(matlab_data, baseline_matlab_data, strict=False): + assert np.array_equal(got["image"], expected["image"]) + assert np.array_equal(got["size"], expected["size"]) + assert np.array_equal(got["joints"][0, 0], expected["joints"][0, 0]) + + +def test_merge_annotateddatasets_drops_likelihood_columns(tmp_path): + scorer = "testscorer" + video_name = "video1" + bodyparts = ["nose", "tail"] + + project_path = tmp_path + labeled_data_dir = project_path / "labeled-data" / video_name + labeled_data_dir.mkdir(parents=True) + + trainingsetfolder_full = project_path / "training-datasets" / "iteration-0" + trainingsetfolder_full.mkdir(parents=True) + + # Build a single-animal annotation dataframe with x/y/likelihood columns + columns = pd.MultiIndex.from_product( + [[scorer], bodyparts, ["x", "y", "likelihood"]], + names=["scorer", "bodyparts", "coords"], + ) + + index = pd.MultiIndex.from_tuples( + [("labeled-data", video_name, "img0001.png")], + ) + + data = np.array([[10.0, 20.0, 0.9, 30.0, 40.0, 0.8]]) + df = pd.DataFrame(data, index=index, columns=columns) + + input_h5 = labeled_data_dir / f"CollectedData_{scorer}.h5" + df.to_hdf(input_h5, key="df_with_missing", mode="w") + + cfg = { + "project_path": str(project_path), + "video_sets": {str(project_path / "videos" / f"{video_name}.mp4"): {}}, + "scorer": scorer, + "bodyparts": bodyparts, + "multianimalproject": False, + } + + merged = trainingsetmanipulation.merge_annotateddatasets( + cfg, + trainingsetfolder_full, + ) + + # Returned dataframe should not contain likelihood anymore + coord_level = "coords" if "coords" in merged.columns.names else merged.columns.names[-1] + assert "likelihood" not in merged.columns.get_level_values(coord_level) + + # Saved merged h5 should also not contain likelihood + output_h5 = trainingsetfolder_full / f"CollectedData_{scorer}.h5" + saved = pd.read_hdf(output_h5) + + coord_level = "coords" if "coords" in saved.columns.names else saved.columns.names[-1] + assert "likelihood" not in saved.columns.get_level_values(coord_level) + + # Sanity check: x/y are preserved + assert set(saved.columns.get_level_values(coord_level)) == {"x", "y"} + output_csv = trainingsetfolder_full / f"CollectedData_{scorer}.csv" + saved_csv = pd.read_csv(output_csv, header=[0, 1, 2], index_col=[0, 1, 2]) + + coord_level = "coords" if "coords" in saved_csv.columns.names else saved_csv.columns.names[-1] + assert "likelihood" not in saved_csv.columns.get_level_values(coord_level) diff --git a/tests/test_triangulation.py b/tests/test_triangulation.py new file mode 100644 index 0000000000..a1b2fe382c --- /dev/null +++ b/tests/test_triangulation.py @@ -0,0 +1,56 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 numpy as np +import pandas as pd +import pytest + +from deeplabcut.pose_estimation_3d import triangulation + + +@pytest.fixture(scope="session") +def stereo_params(): + params = dict() + for i in range(1, 3): + params[f"cameraMatrix{i}"] = np.random.rand(3, 3) + params[f"distCoeffs{i}"] = np.random.rand(1, 5) + params[f"P{i}"] = np.random.rand(3, 4) + params[f"R{i}"] = np.eye(3) + return params + + +def test_undistort_points(stereo_params): + points = np.random.rand(100, 20 * 3) + points_undistorted = triangulation._undistort_points( + points, + stereo_params["cameraMatrix1"], + stereo_params["distCoeffs1"], + stereo_params["P1"], + stereo_params["R1"], + ) + # Test that shape was preserved after vectorization + assert np.shape(points_undistorted) == np.shape(points) + + +@pytest.mark.parametrize( + "n_view_pairs, is_multi", + [(i, flag) for i in range(1, 7, 2) for flag in (False, True)], +) +def test_undistort_views(n_view_pairs, is_multi, stereo_params): + df = pd.read_hdf("tests/data/montblanc_tracks.h5") + if not is_multi: + 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)} + dfs = triangulation._undistort_views(view_pairs, cam_params) + assert len(dfs) == n_view_pairs + assert all(len(pair) == 2 for pair in dfs) + assert len(dfs[0][0].columns.levels) == (4 if is_multi else 3) diff --git a/tests/test_video.py b/tests/test_video.py index 60b33f0e35..7ec4373e88 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -1,16 +1,25 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 + import pytest -from deeplabcut.utils.auxfun_videos import VideoWriter +from deeplabcut.utils.auxfun_videos import VideoWriter -TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") -video = os.path.join(TEST_DATA_DIR, "vid.avi") POS_FRAMES = 1 # Equivalent to cv2.CAP_PROP_POS_FRAMES @pytest.fixture() -def video_clip(): - return VideoWriter(video) +def video_clip(test_data_dir): + return VideoWriter(os.path.join(test_data_dir, "vid.avi")) def test_reader_wrong_inputs(tmp_path): @@ -28,10 +37,10 @@ def test_reader_check_integrity(video_clip): assert os.path.getsize(log_file) == 0 -def test_reader_video_path(video_clip): +def test_reader_video_path(video_clip, test_data_dir): assert video_clip.name == "vid" assert video_clip.format == ".avi" - assert video_clip.directory == TEST_DATA_DIR + assert video_clip.directory == test_data_dir def test_reader_metadata(video_clip): @@ -48,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): @@ -84,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/tests/tools/conftest.py b/tests/tools/conftest.py new file mode 100644 index 0000000000..ef9b94e067 --- /dev/null +++ b/tests/tools/conftest.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import importlib +import sys +from pathlib import Path +from types import ModuleType + +import pytest + + +def _repo_root() -> Path: + # tests/tools/conftest.py -> repo root is 2 levels up + return Path(__file__).resolve().parents[2] + + +def load_selector_module() -> ModuleType: + root = _repo_root() + tools_dir = root / "tools" + init_file = tools_dir / "__init__.py" + selector_path = tools_dir / "test_selector.py" + + if not selector_path.exists(): + raise FileNotFoundError(f"Selector script not found: {selector_path}") + + if not init_file.exists(): + raise FileNotFoundError(f"tools package marker not found: {init_file}") + + # Ensure repo root is importable so `tools.test_selector` resolves as a package import. + root_str = str(root) + if root_str not in sys.path: + sys.path.insert(0, root_str) + + return importlib.import_module("tools.test_selector") + + +@pytest.fixture(scope="session") +def selector(): + """Imported selector module (tools.test_selector).""" + return load_selector_module() diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py new file mode 100644 index 0000000000..c2fcc5cc7c --- /dev/null +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -0,0 +1,674 @@ +from __future__ import annotations + +import importlib.util +import json +import os +import subprocess +from collections.abc import Callable +from datetime import date, datetime, timezone +from pathlib import Path +from types import ModuleType + +import pytest + + +@pytest.fixture(autouse=True) +def no_github_step_summary(monkeypatch): + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + + +# ----------------------------- +# Module loader (tools/ is not necessarily a package) +# ----------------------------- +def load_tool_module() -> ModuleType: + repo_root = Path(__file__).resolve().parents[3] + tool_path = repo_root / "tools" / "docs_and_notebooks_check.py" + assert tool_path.exists(), f"Missing tool: {tool_path}" + + spec = importlib.util.spec_from_file_location("docs_and_notebooks_check", tool_path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[attr-defined] + return mod + + +@pytest.fixture(scope="session") +def tool() -> ModuleType: + return load_tool_module() + + +def _write_default_cfg(repo: Path, include: list[str]) -> Path: + cfg_path = repo / "tools" / "docs_and_notebooks_report_config.yml" + cfg_path.parent.mkdir(parents=True, exist_ok=True) + cfg_path.write_text( + "version: 1\n" + "scan:\n" + " include:\n" + "".join(f" - {pat}\n" for pat in include) + " exclude: []\n" + "policy:\n" + " warn_if_content_older_than_days: 365\n" + " warn_if_verified_older_than_days: 365\n" + " missing_last_verified_is_warning: true\n" + " fail_on_scan_errors: false\n" + " require_metadata: []\n" + " require_recent_verification: []\n" + " require_notebook_normalized: []\n", + encoding="utf-8", + ) + return cfg_path + + +# ----------------------------- +# 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 _git_init(repo: Path) -> None: + _run(["git", "init"], repo) + _run(["git", "config", "user.email", "ci@example.com"], repo) + _run(["git", "config", "user.name", "CI"], repo) + + +def _git_commit(repo: Path, message: str, when_iso: str) -> None: + env = os.environ.copy() + env["GIT_AUTHOR_DATE"] = when_iso + env["GIT_COMMITTER_DATE"] = when_iso + _run(["git", "add", "-A"], repo, env=env) + _run(["git", "commit", "-m", message], repo, env=env) + + +def _write(repo: Path, rel: str, content: str) -> None: + p = repo / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content, encoding="utf-8") + + +# ----------------------------- +# Shared fixtures +# ----------------------------- +@pytest.fixture +def repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _git_init(repo) + return repo + + +@pytest.fixture +def cfg(tool) -> Callable[..., object]: + def _make_cfg( + include: list[str], + exclude: list[str] | None = None, + **policy_overrides, + ): + policy = tool.PolicyConfig(**policy_overrides) + return tool.ToolConfig( + version=1, + scan=tool.ScanConfig(include=include, exclude=exclude or []), + policy=policy, + ) + + return _make_cfg + + +# ----------------------------- +# Contract tests +# ----------------------------- +def test_marker_constants_exist(tool): + assert hasattr(tool, "META_COMMIT_MARKER") + assert hasattr(tool, "SUGGESTED_TAGGED_COMMIT") + assert tool.META_COMMIT_MARKER in tool.SUGGESTED_TAGGED_COMMIT + + +def test_schema_contract_fields(tool): + # DLCMeta must have new fields and must NOT have old last_git_updated + meta = tool.DLCMeta() + assert hasattr(meta, "last_content_updated") + assert hasattr(meta, "last_metadata_updated") + assert hasattr(meta, "last_verified") + assert hasattr(meta, "verified_for") + assert not hasattr(meta, "last_git_updated") + + +def test_git_content_date_skips_meta_commits(tool, repo: Path): + """ + Contract: last_content_updated is computed from git history excluding metadata commits. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + # meta-only rewrite (simulated) committed with marker + _write( + repo, + rel, + "---\ndeeplabcut:\n last_metadata_updated: 2026-03-01\n---\n# hello\n", + ) + _git_commit( + repo, + f"chore(meta): update {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) + + # raw touched date = 2026-03-01 + touched = tool.git_last_touched(repo, rel) + assert touched == date(2026, 3, 1) + + # content updated date should skip marker commit => 2020-01-01 + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2020, 1, 1) + assert used_fallback is False + + +def test_git_content_date_fallback_when_only_meta_commits(tool, repo: Path): + """ + If all commits touching the file are meta-marker commits, we fall back to git_last_touched + and flag used_fallback=True. + """ + rel = "docs/page.md" + _write(repo, rel, "---\ndeeplabcut:\n notes: hi\n---\n") + _git_commit( + repo, + f"chore(meta): init {tool.META_COMMIT_MARKER}", + "2026-03-01T12:00:00+00:00", + ) + + content_date, used_fallback = tool.git_last_content_updated(repo, rel) + assert content_date == date(2026, 3, 1) + assert used_fallback is True + + +def test_scan_is_read_only(tool, repo: Path, cfg): + """ + Contract: report/check (scan_files) must be read-only. + We validate by asserting file content does not change. + """ + rel = "docs/page.md" + orig = "---\ndeeplabcut:\n last_verified: 2020-01-01\n---\n# hello\n" + _write(repo, rel, orig) + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + before = (repo / rel).read_text(encoding="utf-8") + records = tool.scan_files(repo, tool_cfg, targets=[r".\docs\page.md"]) + after = (repo / rel).read_text(encoding="utf-8") + + assert before == after + assert len(records) == 1 + assert records[0].path == rel + assert records[0].kind == "md" + + +def test_scan_targets_support_directory_and_glob(tool, repo: Path, cfg): + rel_a = "docs/gui/napari/basic_usage.md" + rel_b = "docs/gui/napari/advanced_usage.md" + rel_c = "docs/other/overview.md" + + _write(repo, rel_a, "# a\n") + _write(repo, rel_b, "# b\n") + _write(repo, rel_c, "# c\n") + _git_commit(repo, "docs: add pages", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=["docs/**/*.md"]) + + # Directory selector + recs_dir = tool.scan_files(repo, tool_cfg, targets=["docs/gui/napari/"]) + paths_dir = sorted(r.path for r in recs_dir) + assert set(paths_dir) == {rel_a, rel_b} + + # Glob selector + recs_glob = tool.scan_files(repo, tool_cfg, targets=["docs/gui/napari/*.md"]) + paths_glob = sorted(r.path for r in recs_glob) + assert set(paths_glob) == {rel_a, rel_b} + + # Recursive glob selector + recs_recursive = tool.scan_files(repo, tool_cfg, targets=["docs/**/*.md"]) + paths_recursive = sorted(r.path for r in recs_recursive) + assert set(paths_recursive) == {rel_a, rel_b, rel_c} + + +def test_validate_requested_targets_treats_dot_slash_as_unmatched(tool, repo: Path, cfg): + _write(repo, "docs/page.md", "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, ["./"]) + assert matched == [] + assert unmatched == ["./"] + + +def test_validate_requested_targets_treats_empty_like_unmatched(tool, repo: Path, cfg): + _write(repo, "docs/page.md", "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, ["", " "]) + assert matched == [] + assert unmatched == ["", " "] + + +def test_validate_requested_targets_reports_mixed_valid_and_invalid_targets(tool, repo: Path, cfg): + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets(repo, tool_cfg, [rel, "./"]) + assert rel in matched + assert unmatched == ["./"] + + +def test_scan_files_with_invalid_only_targets_matches_nothing(tool, repo: Path, cfg): + tool_cfg = cfg(include=["docs/**/*.md"]) + records = tool.scan_files(repo, tool_cfg, targets=["./"]) + assert records == [] + + +def test_validate_requested_targets_reports_unmatched(tool, repo: Path, cfg): + rel_a = "docs/gui/napari/basic_usage.md" + rel_b = "docs/gui/napari/advanced_usage.md" + + _write(repo, rel_a, "# a\n") + _write(repo, rel_b, "# b\n") + _git_commit(repo, "docs: add pages", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=["docs/**/*.md"]) + + matched, unmatched = tool.validate_requested_targets( + repo, + tool_cfg, + targets=[ + r".\docs\gui\napari\basic_usage.md", + "docs/gui/napari/", + "docs/**/*.md", + "docs/missing/", + "examples/**/*.ipynb", + ], + ) + + assert matched == sorted([rel_a, rel_b]) + assert unmatched == ["docs/missing/", "examples/**/*.ipynb"] + + +def test_update_requires_ack_when_write(tool, repo: Path, cfg): + """ + Contract: write mode should refuse unless --ack-meta-commit-marker is provided. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + # should refuse to write without ack + with pytest.raises(SystemExit): + tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=False, + ) + + +def test_update_set_content_date_from_git_only_changes_that_field(tool, repo: Path, cfg): + """ + 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. + """ + rel = "docs/page.md" + 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") + + tool_cfg = cfg(include=[rel]) + + records = tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=True, + set_last_verified=None, + set_verified_for=None, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + # Read back and confirm verified fields unchanged + text = (repo / rel).read_text(encoding="utf-8") + fm, _body, _ = tool.read_md_frontmatter(text) + assert isinstance(fm, dict) and tool.DLC_NAMESPACE in fm + meta = fm[tool.DLC_NAMESPACE] + + assert meta["last_verified"] == "2020-02-02" + assert meta["verified_for"] == "3.0.0rc1" + + # last_content_updated should reflect git content date (2020-01-01) + assert meta["last_content_updated"] == "2020-01-01" + + # last_metadata_updated should exist because we wrote + assert "last_metadata_updated" in meta + + +def test_update_set_verified_fields_only_changes_verified(tool, repo: Path, cfg): + """ + Contract: update with --set-last-verified / --set-verified-for changes only those fields + (plus last_metadata_updated if writing), and does not set last_content_updated unless requested. + """ + rel = "docs/page.md" + initial = "---\ndeeplabcut:\n last_content_updated: 2000-01-01\n---\n# hello\n" + _write(repo, rel, initial) + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.update_files( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=True, + set_content_date_from_git=False, + set_last_verified=date(2026, 3, 5), + set_verified_for="3.0.0rc13", + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + + text = (repo / rel).read_text(encoding="utf-8") + fm, _body, _ = tool.read_md_frontmatter(text) + meta = fm[tool.DLC_NAMESPACE] + + # Verified fields updated + assert meta["last_verified"] == "2026-03-05" + assert meta["verified_for"] == "3.0.0rc13" + + # last_content_updated remains whatever it was (not overwritten) + assert meta["last_content_updated"] == "2000-01-01" + + +def test_normalize_is_explicit_and_marks_would_change(tool, repo: Path, cfg): + """ + Contract: normalize is separate and explicit; in dry-run it should mark would_change + if notebook is not already in canonical nbformat output. + """ + 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' + _write(repo, rel, raw) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + # Dry-run normalize: should set would_change True if not normalized + records = tool.normalize_notebooks( + repo_root=repo, + cfg=tool_cfg, + targets=[rel], + write=False, + ack_meta_commit_marker=True, + ) + assert len(records) == 1 + assert records[0].kind == "ipynb" + # may be True depending on canonical formatting differences + assert records[0].would_change + + +def test_write_outputs_contract(tool, repo: Path, cfg, tmp_path: Path): + """ + Contract: write_outputs creates both JSON and Markdown files and JSON is schema-valid. + """ + rel = "docs/page.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + + report = tool.Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo), + config_path="in-memory", + totals=tool.summarize(records), + records=records, + ) + + out_dir = tmp_path / "out" + json_path, md_path = tool.write_outputs(report, tool_cfg, out_dir) + + assert json_path.exists() + assert md_path.exists() + + payload = json.loads(json_path.read_text(encoding="utf-8")) + assert payload["schema_version"] == tool.REPORT_SCHEMA_VERSION + assert "records" in payload and isinstance(payload["records"], list) + assert md_path.read_text(encoding="utf-8").startswith("#") + + +def test_notebook_missing_dlc_namespace_warns_missing_metadata(tool, repo: Path, cfg): + 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' + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "missing_metadata" in r.warnings + assert r.meta is None + + +def test_notebook_invalid_dlc_namespace_warns_invalid_metadata(tool, repo: Path, cfg): + rel = "docs/nbs/nb.ipynb" + # deeplabcut namespace exists but is invalid: last_verified must be a date + nb = ( + "{\n" + ' "cells": [],\n' + ' "metadata": {\n' + ' "deeplabcut": {\n' + ' "last_verified": "not-a-date"\n' + " }\n" + " },\n" + ' "nbformat": 4,\n' + ' "nbformat_minor": 5\n' + "}\n" + ) + _write(repo, rel, nb) + _git_commit(repo, "docs: add notebook with bad meta", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + assert len(records) == 1 + r = records[0] + assert r.kind == "ipynb" + assert "invalid_metadata" in r.warnings + assert r.meta is None + + +def test_main_prints_matched_files_and_fails_on_unmatched_targets(tool, repo: Path, monkeypatch, capsys): + rel = "docs/gui/napari/basic_usage.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main( + [ + "--config", + str(cfg_path), + "--no-step-summary", + "report", + "--targets", + r".\docs\gui\napari\basic_usage.md", + "docs/missing/", + ] + ) + + out = capsys.readouterr().out + assert rc == 2 + assert "Matched 1 file(s) from --targets:" in out + assert f"- {rel}" in out + assert "Unmatched --targets:" in out + assert "- docs/missing/" in out + + +def test_main_prints_matched_files_for_valid_targets(tool, repo: Path, monkeypatch, capsys): + rel = "docs/gui/napari/basic_usage.md" + _write(repo, rel, "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main( + [ + "--config", + str(cfg_path), + "--no-step-summary", + "report", + "--targets", + "docs/gui/napari/", + ] + ) + + out = capsys.readouterr().out + assert rc == 0 + assert "Matched 1 file(s) from --targets:" in out + assert f"- {rel}" in out + assert "Report generated:" in out + + +def test_main_returns_2_for_invalid_target_selector(tool, repo: Path, monkeypatch): + _write(repo, "docs/page.md", "# hello\n") + _git_commit(repo, "docs: add page", "2020-01-01T12:00:00+00:00") + cfg_path = _write_default_cfg(repo, include=["docs/**/*.md"]) + + monkeypatch.chdir(repo) + + rc = tool.main(["--config", str(cfg_path), "--no-step-summary", "report", "--targets", "./"]) + assert rc == 2 + + +@pytest.mark.parametrize( + ("rel", "kind"), + [ + ("docs/page.md", "md"), + ("docs/nbs/nb.ipynb", "ipynb"), + ], +) +def test_metadata_sync_warning_populates_report_targets_and_command(tool, repo: Path, cfg, rel: str, kind: str): + """ + Out-of-sync embedded last_content_updated should: + - add metadata_sync_needed warning + - appear in Report.metadata_sync_targets + - generate a non-empty metadata_sync_command + """ + embedded_date = "2000-01-01" + + if kind == "md": + _write( + repo, + rel, + f"---\ndeeplabcut:\n last_content_updated: {embedded_date}\n---\n# hello\n", + ) + else: + nb = tool.nbformat.v4.new_notebook(metadata={tool.DLC_NAMESPACE: {"last_content_updated": embedded_date}}) + _write( + repo, + rel, + tool.nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n", + ) + + # Git-derived content date differs from embedded metadata date + _git_commit(repo, "docs: add file", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel]) + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + + assert len(records) == 1 + rec = records[0] + + # Sanity check: embedded metadata is parsed, but differs from git-derived date + assert rec.meta is not None + assert rec.meta.last_content_updated == date(2000, 1, 1) + assert rec.last_content_updated == date(2020, 1, 1) + + # New warning should be present + assert "metadata_sync_needed" in rec.warnings + + metadata_sync_targets = tool.collect_metadata_sync_targets(records) + metadata_sync_command = tool.build_metadata_sync_command( + "tools/docs_and_notebooks_report_config.yml", + metadata_sync_targets, + ) + + report = tool.Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo), + config_path="tools/docs_and_notebooks_report_config.yml", + totals=tool.summarize(records), + records=records, + metadata_sync_targets=metadata_sync_targets, + metadata_sync_command=metadata_sync_command, + ) + + assert report.metadata_sync_targets == [rel] + assert report.metadata_sync_command + assert "--set-content-date-from-git" in report.metadata_sync_command + assert "--targets" in report.metadata_sync_command + assert rel in report.metadata_sync_command + + # Optional: also verify the markdown report renders the guidance section + rendered = tool.to_markdown(report, tool_cfg) + assert "## Metadata sync suggestions" in rendered + assert f"- `{rel}`" in rendered + + +@pytest.mark.parametrize( + ("rel", "kind"), + [ + ("docs/page.md", "md"), + ("docs/nbs/nb.ipynb", "ipynb"), + ], +) +def test_enforce_fails_when_metadata_sync_needed_is_configured(tool, repo: Path, cfg, rel: str, kind: str): + """ + If policy.fail_if_metadata_sync_needed=True, an out-of-sync file should + become a policy violation in check/enforcement mode. + """ + embedded_date = "2000-01-01" + + if kind == "md": + _write( + repo, + rel, + f"---\ndeeplabcut:\n last_content_updated: {embedded_date}\n---\n# hello\n", + ) + else: + nb = tool.nbformat.v4.new_notebook(metadata={tool.DLC_NAMESPACE: {"last_content_updated": embedded_date}}) + _write( + repo, + rel, + tool.nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n", + ) + + _git_commit(repo, "docs: add file", "2020-01-01T12:00:00+00:00") + + tool_cfg = cfg(include=[rel], fail_if_metadata_sync_needed=True) + records = tool.scan_files(repo, tool_cfg, targets=[rel]) + + violations = tool.enforce(tool_cfg, records) + + assert violations == [ + f"{rel}: embedded last_content_updated is missing or out of sync with git content update date" + ] diff --git a/tests/tools/test_selector/test_selector_decision.py b/tests/tools/test_selector/test_selector_decision.py new file mode 100644 index 0000000000..a7c40cb0a5 --- /dev/null +++ b/tests/tools/test_selector/test_selector_decision.py @@ -0,0 +1,394 @@ +# tests/tools/test_selector/test_selector_decision.py +from __future__ import annotations + +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from tools import test_selector +from tools.test_selector import decide, order_functional_scripts +from tools.test_selector_config import ( + CATEGORY_RULES, + CategoryRule, + prefix, + validate_category_rules, +) + + +def assert_lanes(res, *, skip=False, docs=False, fast=False, full=False): + assert res.lanes.skip is skip + assert res.lanes.docs is docs + assert res.lanes.fast is fast + assert res.lanes.full is full + + +def test_fail_safe_on_empty_changes(selector): + res = selector.decide([]) + + assert_lanes(res, full=True) + assert "no_changed_files_or_diff_unavailable" in res.reasons + assert res.lane_reasons["full"] == ["no_changed_files_or_diff_unavailable"] + + +def test_docs_only(selector): + files = ["docs/index.md", "docs/guide/intro.md", "_config.yml"] + res = selector.decide(files) + + assert_lanes(res, docs=True) + assert res.pytest_paths == [] + assert res.functional_scripts == [] + assert "category:docs" in res.reasons + assert res.lane_reasons["docs"] == ["category:docs"] + + +def test_full_suite_trigger_pyproject_preserves_docs_lane(selector): + files = ["pyproject.toml", "docs/index.md"] + res = selector.decide(files) + + assert_lanes(res, full=True, docs=True) + assert "full_suite_trigger" in res.reasons + assert "category:docs" in res.reasons + assert res.lane_reasons["full"] == [ + "full_suite_trigger", + "full_suite_trigger_count:1", + ] + + +def test_full_suite_trigger_tests_folder(selector): + files = ["tests/test_something.py"] + res = selector.decide(files) + + assert_lanes(res, full=True) + assert "full_suite_trigger" in res.reasons + assert res.lane_reasons["full"] == [ + "full_suite_trigger", + "full_suite_trigger_count:1", + ] + + +def test_fast_core(selector): + files = ["deeplabcut/core/some_module.py"] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + # core rule should include these paths (subset check) + assert "tests/core/" in res.pytest_paths + assert "tests/utils/" in res.pytest_paths + # assert res.functional_scripts == [] # not empty, but we don't need to specify exact scripts here + + assert "category:core" in res.reasons + assert res.lane_reasons["fast"] == ["category:core"] + + # Provenance should attribute selected pytest roots to the core category. + assert "tests/core/" in res.provenance.pytest + assert res.provenance.pytest["tests/core/"] == ["core"] + + +def test_fast_multianimal_includes_functional(selector): + files = ["deeplabcut/pose_estimation_pytorch/multianimal/foo.py"] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + assert "tests/test_predict_multianimal.py" in res.pytest_paths + 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"] + + +def test_fast_ci_workflows_uses_full_suite(selector): + files = [".github/workflows/ci.yml"] + res = selector.decide(files) + + assert_lanes(res, full=True) + + +def test_no_category_matched_is_full(selector): + files = ["some/unknown/place/file.xyz"] + res = selector.decide(files) + + assert_lanes(res, full=True) + assert "no_category_matched" in res.reasons + assert res.lane_reasons["full"] == ["no_category_matched"] + + +def test_docs_and_core_run_both_lanes(selector): + files = ["docs/index.md", "deeplabcut/core/a.py"] + res = selector.decide(files) + + assert_lanes(res, docs=True, fast=True) + assert "category:docs" in res.reasons + assert "category:core" in res.reasons + + assert res.lane_reasons["docs"] == ["category:docs"] + assert res.lane_reasons["fast"] == ["category:core"] + + assert "tests/core/" in res.pytest_paths + assert "tests/utils/" in res.pytest_paths + + +def test_dedup_and_sorted_outputs(selector): + # Force overlap: core includes tests/test_auxiliaryfunctions.py and + # ci_tools contributes tests/tools/. Outputs should stay deduped and sorted. + files = [ + "deeplabcut/core/a.py", + "tools/whatever.py", + ] + res = selector.decide(files) + + assert_lanes(res, fast=True) + + # No duplicates + assert len(res.pytest_paths) == len(set(res.pytest_paths)) + assert len(res.functional_scripts) == len(set(res.functional_scripts)) + + # Sorted + assert res.pytest_paths == sorted(res.pytest_paths) + assert res.functional_scripts == sorted(res.functional_scripts) + + +# ---------------------------- +# Validation of category rules +# ---------------------------- +def test_category_rule_rejects_empty_name(): + with pytest.raises(ValidationError, match="Rule name must not be empty"): + CategoryRule( + name="", + match_any=[prefix("docs/")], + ) + + +def test_category_rule_rejects_invalid_name(): + with pytest.raises(ValidationError, match=r"Rule name must match"): + CategoryRule( + name="docs-rule", + match_any=[prefix("docs/")], + ) + + +def test_category_rule_requires_non_empty_match_any(): + with pytest.raises(ValidationError, match="at least 1 item|at least one predicate"): + CategoryRule( + name="docs", + match_any=[], + ) + + +def test_category_rule_rejects_non_callable_match_any(): + with pytest.raises(ValidationError, match="callable"): + CategoryRule( + name="docs", + match_any=[123], # type: ignore[list-item] + ) + + +@pytest.mark.parametrize( + "field_name,bad_value", + [ + ("pytest_paths", "/absolute/path.py"), + ("pytest_paths", "../escape.py"), + ("functional_scripts", "/absolute/script.py"), + ("functional_scripts", "../escape_script.py"), + ], +) +def test_category_rule_rejects_invalid_repo_relative_paths(field_name, bad_value): + kwargs = { + "name": "docs", + "match_any": [prefix("docs/")], + "pytest_paths": [], + "functional_scripts": [], + } + kwargs[field_name] = [bad_value] + + with pytest.raises(ValidationError, match="repo-relative|path traversal|absolute path"): + CategoryRule(**kwargs) + + +def test_validate_category_rules_rejects_duplicate_names(): + rules = [ + CategoryRule(name="docs", match_any=[prefix("docs/")]), + CategoryRule(name="docs", match_any=[prefix("more-docs/")]), + ] + + with pytest.raises(ValueError, match="Duplicate CategoryRule name"): + validate_category_rules(rules) + + +def test_lint_only_changes_select_skip_lane(selector): + files = [".pre-commit-config.yaml"] + res = selector.decide(files) + + assert_lanes(res, skip=True) + assert res.pytest_paths == [] + assert res.functional_scripts == [] + + assert "lint_only" in res.reasons + assert "skip" in res.lane_reasons + assert "lint_only" in res.lane_reasons["skip"] + + +def test_validate_selected_paths_escalates_to_full_on_missing(selector, tmp_path: Path): + # Build a minimal repo dir with none of the selected paths present. + repo = tmp_path / "repo" + repo.mkdir() + + res = selector.SelectorResult( + lanes=selector.LaneSelection(fast=True), + pytest_paths=["tests/does_not_exist.py"], + functional_scripts=["examples/missing_script.py"], + provenance=selector.SelectionProvenance( + pytest={"tests/does_not_exist.py": ["core"]}, + scripts={"examples/missing_script.py": ["core"]}, + ), + reasons=["category:core"], + changed_files=["deeplabcut/core/foo.py"], + lane_reasons={"fast": ["category:core"]}, + ) + + out = selector.validate_selected_paths(res, repo) + + assert out.lanes.fast is False + assert out.lanes.full is True + + assert out.pytest_paths == [] + assert out.functional_scripts == [] + assert out.provenance.pytest == {} + assert out.provenance.scripts == {} + + assert "missing_selected_paths" in out.reasons + assert any(r.startswith("pytest:tests/does_not_exist.py") for r in out.reasons) + assert any(r.startswith("script:examples/missing_script.py") for r in out.reasons) + + assert "full" in out.lane_reasons + + +def test_validate_selected_paths_keeps_fast_when_paths_exist(selector, tmp_path: Path): + repo = tmp_path / "repo" + (repo / "tests").mkdir(parents=True) + (repo / "examples").mkdir(parents=True) + + (repo / "tests" / "test_ok.py").write_text("def test_ok(): pass\n") + (repo / "examples" / "script_ok.py").write_text("print('ok')\n") + + res = selector.SelectorResult( + lanes=selector.LaneSelection(fast=True), + pytest_paths=["tests/test_ok.py"], + functional_scripts=["examples/script_ok.py"], + provenance=selector.SelectionProvenance( + pytest={"tests/test_ok.py": ["core"]}, + scripts={"examples/script_ok.py": ["core"]}, + ), + reasons=["category:core"], + changed_files=["deeplabcut/core/foo.py"], + lane_reasons={"fast": ["category:core"]}, + ) + + out = selector.validate_selected_paths(res, repo) + + assert out.lanes.fast is True + assert out.lanes.full is False + assert out.pytest_paths == ["tests/test_ok.py"] + assert out.functional_scripts == ["examples/script_ok.py"] + + +# -------------------------------------- +# Current config validity & sanity checks +# -------------------------------------- + + +def test_current_category_rules_are_typed_models(): + assert CATEGORY_RULES + assert all(isinstance(rule, CategoryRule) for rule in CATEGORY_RULES) + + +def test_current_category_rules_pass_cross_rule_validation(): + validate_category_rules(CATEGORY_RULES) + + +def test_current_category_rule_names_are_unique(): + names = [rule.name for rule in CATEGORY_RULES] + assert len(names) == len(set(names)) + + +def test_current_category_rules_have_matchers(): + assert all(rule.match_any for rule in CATEGORY_RULES) + + +def test_required_category_rules_exist(): + names = {rule.name for rule in CATEGORY_RULES} + assert "docs" in names + assert "core" in names + + +def test_docs_rule_exists_once(): + docs_rules = [rule for rule in CATEGORY_RULES if rule.name == "docs"] + assert len(docs_rules) == 1 + + +def test_current_selected_paths_exist(): + repo_root = Path(__file__).resolve().parents[3] + missing = [] + + for rule in CATEGORY_RULES: + for path in rule.pytest_paths: + if not (repo_root / path).exists(): + missing.append((rule.name, "pytest", path)) + for path in rule.functional_scripts: + if not (repo_root / path).exists(): + missing.append((rule.name, "script", path)) + + assert missing == [] + + +def test_3d_script_includes_tensorflow_prerequisite(): + scripts = order_functional_scripts({"examples/testscript_3d.py"}) + + assert scripts == [ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_3d.py", + ] + + +def test_explicit_dependency_is_not_duplicated(): + scripts = order_functional_scripts( + { + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_3d.py", + } + ) + + assert scripts == [ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_3d.py", + ] + + +def test_functional_script_dependency_cycle(monkeypatch): + monkeypatch.setattr( + test_selector, + "FUNC_SCRIPT_DEPENDENCIES", + { + "a.py": ("b.py",), + "b.py": ("a.py",), + }, + ) + + with pytest.raises(ValueError, match="Cyclic"): + order_functional_scripts({"a.py"}) + + +def test_3d_changes_select_scripts_in_dependency_order(): + result = decide(["deeplabcut/pose_estimation_3d/plotting3D.py"]) + + assert result.functional_scripts == [ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_3d.py", + ] + assert ( + "dependency:examples/testscript_3d.py" + in result.provenance.scripts["examples/testscript_tensorflow_single_animal.py"] + ) + assert "3d_pose_estimation" in result.provenance.scripts["examples/testscript_3d.py"] diff --git a/tests/tools/test_selector/test_selector_validation.py b/tests/tools/test_selector/test_selector_validation.py new file mode 100644 index 0000000000..41d2bfe912 --- /dev/null +++ b/tests/tools/test_selector/test_selector_validation.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + + +# ----------------- +# Git helpers +# ----------------- +def _git(repo: Path, *args: str) -> str: + proc = subprocess.run( + ["git", *args], + cwd=repo, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(f"git {' '.join(args)} failed: {proc.stderr.strip()}") + return proc.stdout.strip() + + +def _init_repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Test User") + _git(repo, "config", "user.email", "test@example.com") + return repo + + +def _commit_file(repo: Path, relpath: str, content: str, message: str) -> str: + path = repo / relpath + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + _git(repo, "add", relpath) + _git(repo, "commit", "-m", message) + return _git(repo, "rev-parse", "HEAD") + + +def _write_event(tmp_path: Path, payload: dict) -> Path: + event_path = tmp_path / "event.json" + event_path.write_text(json.dumps(payload), encoding="utf-8") + return event_path + + +# -------------- +# SHA validation & diff range parsing +# -------------- +def test_validate_sha_accepts(selector): + assert selector._validate_sha("x", "abc1234") == "abc1234" + assert selector._validate_sha("x", "a" * 40) == "a" * 40 + + +@pytest.mark.parametrize( + "bad", + [ + "", # empty + "notasha", # non-hex + "123", # too short + "g" * 40, # non-hex + " " * 8, # whitespace + ], +) +def test_validate_sha_rejects(selector, bad): + with pytest.raises(ValueError): + selector._validate_sha("x", bad) + + +def test_determine_diff_range_pr_uses_merge_base(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + merge_base = _commit_file(repo, "shared.txt", "base", "base commit") + base_sha = _commit_file(repo, "main.txt", "main", "main branch commit") + + _git(repo, "checkout", "-b", "feature", merge_base) + head_sha = _commit_file(repo, "feature.txt", "feature", "feature branch commit") + + event_path = _write_event( + tmp_path, + { + "pull_request": { + "base": {"sha": base_sha}, + "head": {"sha": head_sha}, + } + }, + ) + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == merge_base + assert head == head_sha + assert mode == selector.DiffMode.PR + + +def test_determine_diff_range_push_uses_before_after(selector, tmp_path, monkeypatch): + repo = _init_repo(tmp_path) + + before = _commit_file(repo, "a.txt", "one", "first commit") + after = _commit_file(repo, "a.txt", "two", "second commit") + + event_path = _write_event(tmp_path, {"before": before, "after": after}) + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == before + assert head == after + assert mode == selector.DiffMode.PUSH + + +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") + zero_sha = "0" * 40 + event_path = _write_event(tmp_path, {"before": zero_sha, "after": after}) + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_path)) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == selector._empty_tree(repo) + assert head == after + assert mode == selector.DiffMode.INITIAL + + +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") + head_sha = _commit_file(repo, "a.txt", "two", "second commit") + + monkeypatch.delenv("GITHUB_EVENT_NAME", raising=False) + monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False) + + base, head, mode = selector.determine_diff_range(repo, None, None) + + assert base == prev + assert head == head_sha + assert mode == selector.DiffMode.FALLBACK + + +# ----------------- +# Paths +# ----------------- +def test_normalize_relpath_basic(selector): + assert selector._normalize_relpath("docs/index.md") == "docs/index.md" + assert selector._normalize_relpath("docs\\index.md") == "docs/index.md" + + +@pytest.mark.parametrize( + "bad", + [ + "", # empty + " ", # whitespace + "/etc/passwd", # absolute unix + "C:/Windows/x", # absolute windows + "../secret.txt", # traversal + "docs/../../x", # traversal inside + "a\x00b", # NUL + ], +) +def test_normalize_relpath_rejects_bad(selector, bad): + with pytest.raises(ValueError): + selector._normalize_relpath(bad) diff --git a/tests/utils/test_collect_video_paths.py b/tests/utils/test_collect_video_paths.py new file mode 100644 index 0000000000..c964db9918 --- /dev/null +++ b/tests/utils/test_collect_video_paths.py @@ -0,0 +1,207 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +"""Tests for ``collect_video_paths``. + +These tests pin down the rule: + +* When ``video_type`` is not set, directory enumeration filters by + ``SUPPORTED_VIDEOS`` but explicitly-supplied files are trusted (returned + as-is, even if they have no suffix). +* When ``video_type`` is set, it is honoured everywhere — both for files + pulled from directories and for files supplied by the caller. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from deeplabcut.core.deprecation import DLCDeprecationWarning +from deeplabcut.utils.auxfun_videos import SUPPORTED_VIDEOS, collect_video_paths + + +def _touch(path: Path) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"") + return path + + +def test_keeps_suffixless_files_when_explicitly_listed(tmp_path): + """Regression test: a caller-supplied file without an extension (e.g. + a content-addressed cache entry) must not be silently dropped.""" + suffixed = _touch(tmp_path / "video.mp4") + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths([suffixed, hashed], extensions=None) + + assert {p.name for p in result} == {"video.mp4", "abcd1234"} + + +def test_accepts_path_objects_and_strings(tmp_path): + suffixed = _touch(tmp_path / "video.mp4") + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths([str(suffixed), hashed], extensions=None) + + assert {p.name for p in result} == {"video.mp4", "abcd1234"} + + +def test_accepts_single_path_argument(tmp_path): + """A single path (not wrapped in a list) is also valid input.""" + hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths(hashed, extensions=None) + + assert [p.name for p in result] == ["abcd1234"] + + +def test_explicit_video_type_filters_listed_files(tmp_path): + """When ``extensions`` is set, it filters explicitly-supplied files too.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions="mp4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_explicit_video_type_accepts_leading_dot(tmp_path): + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions=".mp4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_explicit_video_type_case_insensitive(tmp_path): + """Extension matching must be case-insensitive.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + + result = collect_video_paths([mp4, avi], extensions="MP4") + + assert {p.name for p in result} == {"video.mp4"} + + +def test_multiple_extensions_filter_directory(tmp_path): + """A sequence of extensions filters directory contents to only matching files.""" + mp4 = _touch(tmp_path / "video.mp4") + avi = _touch(tmp_path / "video.avi") + _touch(tmp_path / "video.mkv") + + result = collect_video_paths(tmp_path, extensions=["mp4", "avi"]) + + assert {p.name for p in result} == {mp4.name, avi.name} + + +def test_directory_enumeration_filters_by_supported_videos(tmp_path): + """Directory scans must continue to discriminate videos from non-videos.""" + mp4 = _touch(tmp_path / "video.mp4") + _touch(tmp_path / "notes.txt") + _touch(tmp_path / "results.h5") + _touch(tmp_path / "abcd1234") # suffix-less file in a directory: not a video + + result = collect_video_paths(tmp_path, extensions=None) + + assert [p.name for p in result] == [mp4.name] + + +def test_directory_enumeration_skips_dlc_artifacts(tmp_path): + """``*_labeled.*`` and ``*_full.*`` are DLC outputs, not inputs.""" + mp4 = _touch(tmp_path / "video.mp4") + _touch(tmp_path / "video_labeled.mp4") + _touch(tmp_path / "video_full.mp4") + + result = collect_video_paths(tmp_path, extensions=None) + + assert {p.name for p in result} == {mp4.name} + + +def test_disable_exclude_patterns_includes_dlc_artifacts(tmp_path): + """Setting ``exclude_patterns=[]`` disables all pattern exclusion.""" + mp4 = _touch(tmp_path / "video.mp4") + labeled = _touch(tmp_path / "video_labeled.mp4") + full = _touch(tmp_path / "video_full.mp4") + + result = collect_video_paths(tmp_path, extensions=None, exclude_patterns=[]) + + assert {p.name for p in result} == {mp4.name, labeled.name, full.name} + + +def test_mixed_files_and_directories(tmp_path): + """The function handles a mix of explicit files and directories.""" + folder = tmp_path / "folder" + in_folder = _touch(folder / "from_dir.mp4") + _touch(folder / "ignored.txt") + + explicit_mp4 = _touch(tmp_path / "explicit.mp4") + explicit_hashed = _touch(tmp_path / "abcd1234") + + result = collect_video_paths( + [folder, explicit_mp4, explicit_hashed], + extensions=None, + ) + + assert {p.name for p in result} == { + in_folder.name, + explicit_mp4.name, + explicit_hashed.name, + } + + +def test_duplicates_are_removed(tmp_path): + mp4 = _touch(tmp_path / "video.mp4") + + result = collect_video_paths([mp4, mp4, str(mp4)], extensions=None) + + assert len(result) == 1 + assert result[0].name == "video.mp4" + + +def test_missing_path_raises(tmp_path): + with pytest.raises(FileNotFoundError): + collect_video_paths([tmp_path / "does_not_exist.mp4"], extensions=None) + + +@pytest.mark.parametrize("ext", SUPPORTED_VIDEOS) +def test_each_supported_extension_picked_up_in_directory(tmp_path, ext): + expected = _touch(tmp_path / f"clip.{ext}") + + result = collect_video_paths(tmp_path, extensions=None) + + assert [p.name for p in result] == [expected.name] + + +def test_sorted_by_default_when_not_shuffled(tmp_path): + a = _touch(tmp_path / "a.mp4") + b = _touch(tmp_path / "b.mp4") + c = _touch(tmp_path / "c.mp4") + + result = collect_video_paths([c, a, b], extensions=None, shuffle=False) + + assert [p.name for p in result] == ["a.mp4", "b.mp4", "c.mp4"] + + +@pytest.mark.parametrize("deprecated_value", ["", [""], ("",), {""}]) +def test_deprecated_empty_extensions_warns(tmp_path, deprecated_value): + """Empty / blank extension values are deprecated and should emit a warning.""" + _touch(tmp_path / "video.mp4") + + with pytest.warns(DLCDeprecationWarning): + collect_video_paths(tmp_path, extensions=deprecated_value) + + +def test_empty_sequence_raises(tmp_path): + """An empty sequence is not a valid filter; callers must pass None instead.""" + with pytest.raises(ValueError): + collect_video_paths(tmp_path, extensions=[]) diff --git a/tests/utils/test_deprecation.py b/tests/utils/test_deprecation.py new file mode 100644 index 0000000000..6b97b06190 --- /dev/null +++ b/tests/utils/test_deprecation.py @@ -0,0 +1,291 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 +# +import warnings + +import pytest +from packaging.version import Version + +from deeplabcut.core.deprecation import ( + DLCDeprecationWarning, + deprecated, + renamed_parameter, +) + +# --------------------------------------------------------------------------- +# @deprecated +# --------------------------------------------------------------------------- + + +def test_deprecated_emits_deprecation_warning(): + @deprecated() + def old_fn(): + return 42 + + with pytest.warns(DLCDeprecationWarning): + result = old_fn() + + assert result == 42 + + +def test_deprecated_warning_contains_function_name(): + @deprecated() + def my_old_function(): + pass + + with pytest.warns(DLCDeprecationWarning, match="my_old_function"): + my_old_function() + + +def test_deprecated_warning_contains_replacement(): + @deprecated(replacement="new_module.new_fn") + def old_fn(): + pass + + with pytest.warns(DLCDeprecationWarning, match="new_module.new_fn"): + old_fn() + + +def test_deprecated_warning_contains_since_and_removed_in(): + @deprecated(since="3.1", removed_in="4.0") + def old_fn(): + pass + + with pytest.warns(DLCDeprecationWarning, match="3.1") as record: + old_fn() + + assert "4.0" in str(record[0].message) + + +def test_deprecated_preserves_return_value_and_args(): + @deprecated() + def add(a, b): + return a + b + + with pytest.warns(DLCDeprecationWarning): + assert add(2, 3) == 5 + + +def test_deprecated_preserves_name_and_docstring(): + @deprecated(replacement="new_fn") + def documented_fn(): + """Original docstring.""" + + assert documented_fn.__name__ == "documented_fn" + assert "Original docstring." in documented_fn.__doc__ + assert "Deprecated." in documented_fn.__doc__ + assert "new_fn" in documented_fn.__doc__ + + +def test_deprecated_attaches_metadata(): + @deprecated(replacement="new_fn", since="3.1", removed_in="4.0") + def old_fn(): + pass + + info = old_fn.__deprecated_info__ + assert info.kind == "callable" + assert info.target.endswith("old_fn") + assert info.replacement == "new_fn" + assert info.since == Version("3.1") + assert info.removed_in == Version("4.0") + + +def test_deprecated_invalid_since_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @deprecated(since="not-a-version") + def old_fn(): + pass + + +def test_deprecated_invalid_removed_in_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @deprecated(removed_in="definitely-not-a-version") + def old_fn(): + pass + + +def test_deprecated_removed_in_must_be_greater_than_since(): + with pytest.raises(ValueError, match="must be greater than"): + + @deprecated(since="4.0", removed_in="4.0") + def old_fn(): + pass + + +# --------------------------------------------------------------------------- +# @renamed_parameter +# --------------------------------------------------------------------------- + + +def test_renamed_parameter_old_name_emits_warning(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with pytest.warns(DLCDeprecationWarning): + fn(in_random_order=True) + + +def test_renamed_parameter_old_name_is_forwarded(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with pytest.warns(DLCDeprecationWarning): + result = fn(in_random_order=True) + + assert result is True + + +def test_renamed_parameter_new_name_no_warning(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + # No warning should be emitted when using the current name. + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + result = fn(shuffle=True) + + assert result is True + + +def test_renamed_parameter_warning_contains_names(): + @renamed_parameter(old="videotype", new="video_extensions", since="3.2") + def fn(video_extensions=None): + return video_extensions + + with pytest.warns(DLCDeprecationWarning, match="videotype") as record: + fn(videotype="mp4") + + message = str(record[0].message) + assert "video_extensions" in message + assert "3.2" in message + + +def test_renamed_parameter_preserves_name(): + @renamed_parameter(old="foo", new="bar") + def my_fn(bar=None): + """Docstring.""" + + assert my_fn.__name__ == "my_fn" + + +def test_renamed_parameter_old_and_new_together_raise(): + @renamed_parameter(old="videotype", new="video_extensions") + def fn(video_extensions=None): + return video_extensions + + with pytest.raises(TypeError, match="both 'videotype' and 'video_extensions'"): + fn(videotype="mp4", video_extensions="avi") + + +def test_renamed_parameter_attaches_metadata(): + @renamed_parameter(old="videotype", new="video_extensions", since="3.2") + def fn(video_extensions=None): + return video_extensions + + params = fn.__deprecated_params__ + assert len(params) == 1 + + info = params[0] + assert info.kind == "parameter" + assert info.target.endswith("fn") + assert info.old_parameter == "videotype" + assert info.new_parameter == "video_extensions" + assert info.since == Version("3.2") + + +def test_renamed_parameter_invalid_since_raises(): + with pytest.raises(ValueError, match="Invalid version"): + + @renamed_parameter(old="videotype", new="video_extensions", since="invalid-version") + def fn(video_extensions=None): + return video_extensions + + +def test_renamed_parameter_new_not_in_signature_raises(): + with pytest.raises(ValueError, match="not a parameter"): + + @renamed_parameter(old="foo", new="nonexistent") + def fn(bar=None): + return bar + + +def test_new_not_in_signature_raises(): + """Applying a rename whose 'new' is not in the signature raises an error.""" + with pytest.raises(ValueError, match="not a parameter"): + + @renamed_parameter(old="old_name", new="new_name") + def fn(not_new_name=None): + return not_new_name + + +def test_old_still_in_signature_raises(): + """Applying a rename when the old name is still in the signature raises an error.""" + with pytest.raises(ValueError, match="still a parameter"): + + @renamed_parameter(old="old_name", new="new_name") + def fn(old_name=None, new_name=None): + return new_name + + +def test_renamed_parameter_chaining_raises(): + """Chaining renames A→B→C raises an error.""" + with pytest.raises(ValueError, match="chaining renames is not allowed"): + + @renamed_parameter(old="A", new="B") # outer: A→B, but B is already deprecated to C + @renamed_parameter(old="B", new="C") # inner: B→C + def fn(C=None): + return C + + +def test_renamed_parameter_multiple_independent_renames(): + @renamed_parameter(old="batchsize", new="batch_size") + @renamed_parameter(old="videotype", new="video_extensions") + def fn(video_extensions=None, batch_size=None): + return video_extensions, batch_size + + with pytest.warns(DLCDeprecationWarning): + result = fn(videotype="mp4") + assert result == ("mp4", None) + + with pytest.warns(DLCDeprecationWarning): + result = fn(batchsize=4) + assert result == (None, 4) + + +def test_renamed_parameter_positional_arg_unaffected(): + @renamed_parameter(old="in_random_order", new="shuffle") + def fn(shuffle=False): + return shuffle + + with warnings.catch_warnings(): + warnings.simplefilter("error", DLCDeprecationWarning) + result = fn(True) + + assert result is True + + +def test_multiple_subsequent_renames_allowed(): + @renamed_parameter(old="oldestname", new="newest", since="3.0.0") + @renamed_parameter(old="older_name", new="newest", since="4.0.0") + def fn(*, newest): + return newest + + with pytest.warns(DLCDeprecationWarning): + result = fn(oldestname=1) + assert result == 1 + + with pytest.warns(DLCDeprecationWarning): + result = fn(older_name=2) + assert result == 2 diff --git a/tests/utils/test_multiprocessing.py b/tests/utils/test_multiprocessing.py new file mode 100644 index 0000000000..a5ab47dd5f --- /dev/null +++ b/tests/utils/test_multiprocessing.py @@ -0,0 +1,40 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. 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 time + +import pytest + +from deeplabcut.utils.multiprocessing import call_with_timeout + + +def _succeeding_method(parameter): + return parameter + + +def _failing_method(): + raise ValueError("Raise value error on purpose") + + +def _hanging_method(): + while True: + time.sleep(5) + + +@pytest.mark.skip(reason="Flaky on CI - imports that can exceed timeout on resource-constrained systems") +def test_call_with_timeout(): + parameter = (10, "Hello test") + assert call_with_timeout(_succeeding_method, 30, parameter) == parameter + + with pytest.raises(ValueError): + call_with_timeout(_failing_method, timeout=30) + + with pytest.raises(TimeoutError): + call_with_timeout(_hanging_method, timeout=1) diff --git a/tests/utils/test_skeleton.py b/tests/utils/test_skeleton.py new file mode 100644 index 0000000000..deba4d3c07 --- /dev/null +++ b/tests/utils/test_skeleton.py @@ -0,0 +1,378 @@ +import warnings +from types import SimpleNamespace + +import matplotlib + +matplotlib.use("Agg", force=True) + +import numpy as np +import pandas as pd +import pytest +from matplotlib.collections import LineCollection +from matplotlib.figure import Figure +from scipy.spatial import KDTree + +from deeplabcut.utils import skeleton as skeleton_mod +from deeplabcut.utils.skeleton import SkeletonBuilder, write_config + +# --------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------- + + +def make_config(project_path, scorer="TestScorer", skeleton=None): + return { + "project_path": str(project_path), + "scorer": scorer, + "skeleton": skeleton or [], + "skeleton_color": "red", + "dotsize": 4, + } + + +def make_test_builder(): + """ + Construct a SkeletonBuilder instance without calling __init__, + so individual methods can be unit-tested in isolation. + """ + builder = SkeletonBuilder.__new__(SkeletonBuilder) + return builder + + +def attach_fake_canvas(builder): + builder.fig = Figure() + builder._ax = builder.fig.add_subplot(111) + builder._ax.set_xlim(-5, 25) + builder._ax.set_ylim(-5, 5) + builder.fig.canvas.draw_idle = lambda: None + + +# --------------------------------------------------------------------- +# pick_labeled_frame +# --------------------------------------------------------------------- + + +def test_pick_labeled_frame_multi_animal_drops_single(monkeypatch): + builder = make_test_builder() + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["single", "mouseA"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + + # "single" is fully labeled too, but should be dropped before choosing. + row = [ + 1.0, + 2.0, + 3.0, + 4.0, # single + 10.0, + 20.0, + 30.0, + 40.0, # mouseA + ] + builder.df = pd.DataFrame([row], index=index, columns=columns) + + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + picked_row, picked_col = builder.pick_labeled_frame() + + assert picked_row == ("labeled-data/session1", "img001.png") + assert picked_col == "mouseA" + + +def test_pick_labeled_frame_without_individuals(monkeypatch): + builder = make_test_builder() + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + + builder.df = pd.DataFrame( + [[1.0, 2.0, 3.0, 4.0]], + index=index, + columns=columns, + ) + + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + picked_row, picked_col = builder.pick_labeled_frame() + + assert picked_row == ("labeled-data/session1", "img001.png") + # fallback path uses count(...).to_frame(), so the single column is usually 0 + assert picked_col == 0 + + +# --------------------------------------------------------------------- +# clear +# --------------------------------------------------------------------- + + +def test_clear_resets_indices_segments_and_linecollection(): + builder = make_test_builder() + builder.inds = {(0, 1), (1, 2)} + builder.segs = { + ((0.0, 0.0), (10.0, 0.0)), + ((10.0, 0.0), (20.0, 0.0)), + } + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]]), np.array([[10.0, 0.0], [20.0, 0.0]])]) + attach_fake_canvas(builder) + + builder.clear() + + assert builder.inds == set() + assert builder.segs == set() + assert list(builder.lines.get_segments()) == [] + + +# --------------------------------------------------------------------- +# export +# --------------------------------------------------------------------- + + +def test_export_sorts_pairs_and_warns_for_unconnected(monkeypatch, caplog): + builder = make_test_builder() + builder.config_path = "dummy_config.yaml" + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + [30.0, 0.0], # intentionally left unconnected + ] + ) + builder.bpts = pd.Index(["nose", "tail", "paw", "ear"], name="bodyparts") + builder.inds = {(1, 2), (0, 1)} # intentionally unordered + builder.cfg = {"skeleton": []} + + captured = {} + + def fake_write_config(path, cfg): + captured["path"] = path + captured["cfg"] = cfg.copy() + + monkeypatch.setattr(skeleton_mod, "write_config", fake_write_config) + + with caplog.at_level("INFO"): + builder.export() + assert "Not all bodyparts are connected" in caplog.text + + assert captured["path"] == "dummy_config.yaml" + assert captured["cfg"]["skeleton"] == [ + ("nose", "tail"), + ("tail", "paw"), + ] + + +def test_export_without_warning_when_all_bodyparts_connected(monkeypatch): + builder = make_test_builder() + builder.config_path = "dummy_config.yaml" + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.bpts = pd.Index(["nose", "tail", "paw"], name="bodyparts") + builder.inds = {(0, 1), (1, 2)} + builder.cfg = {"skeleton": []} + + monkeypatch.setattr(skeleton_mod, "write_config", lambda path, cfg: None) + + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + builder.export() + + assert not any("didn't connect all the bodyparts" in str(w.message) for w in record) + assert builder.cfg["skeleton"] == [ + ("nose", "tail"), + ("tail", "paw"), + ] + + +# --------------------------------------------------------------------- +# on_select +# --------------------------------------------------------------------- + + +def test_on_select_adds_pairs_segments_and_updates_canvas(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = set() + builder.segs = set() + builder.lines = LineCollection([]) + attach_fake_canvas(builder) + + verts = [(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)] + builder.on_select(verts) + + assert builder.inds == {(0, 1), (1, 2)} + assert ((0.0, 0.0), (10.0, 0.0)) in builder.segs + assert ((10.0, 0.0), (20.0, 0.0)) in builder.segs + assert len(builder.lines.get_segments()) == 2 + + +def test_on_select_ignores_duplicate_hits(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + [20.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = set() + builder.segs = set() + builder.lines = LineCollection([]) + attach_fake_canvas(builder) + + # Repeated nearby vertices should not create duplicate pairs + verts = [(0.0, 0.0), (0.1, 0.0), (10.0, 0.0), (10.1, 0.0), (20.0, 0.0)] + builder.on_select(verts) + + assert builder.inds == {(0, 1), (1, 2)} + assert len(builder.segs) == 2 + + +# --------------------------------------------------------------------- +# on_pick +# --------------------------------------------------------------------- + + +def test_on_pick_right_click_removes_segment_and_pair(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = {(0, 1)} + builder.segs = {((0.0, 0.0), (10.0, 0.0))} + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]])]) + attach_fake_canvas(builder) + + event = SimpleNamespace( + mouseevent=SimpleNamespace(button=3), + artist=builder.lines, + ind=[0], + ) + + builder.on_pick(event) + + assert builder.inds == set() + assert builder.segs == set() + assert list(builder.lines.get_segments()) == [] + + +def test_on_pick_non_right_click_does_nothing(): + builder = make_test_builder() + builder.xy = np.array( + [ + [0.0, 0.0], + [10.0, 0.0], + ] + ) + builder.tree = KDTree(builder.xy) + builder.inds = {(0, 1)} + builder.segs = {((0.0, 0.0), (10.0, 0.0))} + builder.lines = LineCollection([np.array([[0.0, 0.0], [10.0, 0.0]])]) + attach_fake_canvas(builder) + + event = SimpleNamespace( + mouseevent=SimpleNamespace(button=1), + artist=builder.lines, + ind=[0], + ) + + builder.on_pick(event) + + assert builder.inds == {(0, 1)} + assert builder.segs == {((0.0, 0.0), (10.0, 0.0))} + assert len(builder.lines.get_segments()) == 1 + + +# --------------------------------------------------------------------- +# __init__ lightweight integration +# --------------------------------------------------------------------- + + +def test_init_loads_dataframe_image_and_existing_skeleton(tmp_path, monkeypatch): + project_path = tmp_path / "project" + labeled_data = project_path / "labeled-data" / "session1" + labeled_data.mkdir(parents=True) + + cfg_path = project_path / "config.yaml" + cfg = make_config( + project_path=project_path, + scorer="TestScorer", + skeleton=[ + ["nose", "tail"], + ["missing", "nose"], + ], # second pair should be ignored + ) + write_config(cfg_path, cfg) + + index = pd.MultiIndex.from_tuples( + [("labeled-data/session1", "img001.png")], + names=["folder", "image"], + ) + columns = pd.MultiIndex.from_product( + [["TestScorer"], ["nose", "tail"], ["x", "y"]], + names=["scorer", "bodyparts", "coords"], + ) + df = pd.DataFrame( + [[0.0, 0.0, 10.0, 0.0]], + index=index, + columns=columns, + ) + h5_path = labeled_data / "CollectedData_TestScorer.h5" + df.to_hdf(h5_path, key="df", mode="w") + + monkeypatch.setattr(skeleton_mod.io, "imread", lambda path: np.zeros((5, 5, 3), dtype=np.uint8)) + monkeypatch.setattr(SkeletonBuilder, "build_ui", lambda self: None) + monkeypatch.setattr(SkeletonBuilder, "display", lambda self: None) + monkeypatch.setattr(np.random, "shuffle", lambda x: None) + + builder = SkeletonBuilder(str(cfg_path)) + + assert builder.config_path == str(cfg_path) + assert list(builder.bpts) == ["nose", "tail"] + assert builder.xy.shape == (2, 2) + assert builder.image.shape == (5, 5, 3) + assert builder.inds == {(0, 1)} + assert ((0.0, 0.0), (10.0, 0.0)) in builder.segs + + +def test_init_raises_if_no_labeled_data_found(tmp_path, monkeypatch): + project_path = tmp_path / "project" + (project_path / "labeled-data").mkdir(parents=True) + + cfg_path = project_path / "config.yaml" + cfg = make_config(project_path=project_path, scorer="TestScorer") + write_config(cfg_path, cfg) + + monkeypatch.setattr(SkeletonBuilder, "build_ui", lambda self: None) + monkeypatch.setattr(SkeletonBuilder, "display", lambda self: None) + + with pytest.raises(IOError, match="No labeled data were found"): + SkeletonBuilder(str(cfg_path)) diff --git a/tests/utils/test_video_processor.py b/tests/utils/test_video_processor.py new file mode 100644 index 0000000000..81a97a09fe --- /dev/null +++ b/tests/utils/test_video_processor.py @@ -0,0 +1,194 @@ +import cv2 +import numpy as np +import pytest + +from deeplabcut.utils.video_processor import VideoProcessorCV + + +def _make_test_video(path, nframes=3, width=8, height=6, fps=10.0, codec="mp4v"): + """Create a small RGB-ish test video using OpenCV's BGR writer.""" + fourcc = cv2.VideoWriter_fourcc(*codec) + writer = cv2.VideoWriter(str(path), fourcc, fps, (width, height), True) + assert writer.isOpened(), "Could not create temporary test video." + + frames_rgb = [] + for i in range(nframes): + frame_rgb = np.zeros((height, width, 3), dtype=np.uint8) + frame_rgb[..., 0] = 10 + i # R + frame_rgb[..., 1] = 20 + i # G + frame_rgb[..., 2] = 30 + i # B + frames_rgb.append(frame_rgb.copy()) + + # OpenCV expects BGR. + writer.write(np.flip(frame_rgb, axis=2)) + + writer.release() + return frames_rgb + + +def test_video_processor_cv_reads_basic_metadata(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=4, width=12, height=10, fps=15.0) + + clip = VideoProcessorCV(fname=str(video_path)) + + try: + assert clip.width == 12 + assert clip.height == 10 + assert clip.nframes == 4 + assert clip.i == 0 + assert clip.fps > 0 + assert clip.nc == 3 + finally: + clip.close() + + +def test_video_processor_cv_load_frame_returns_rgb_and_increments_counter(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=2, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path)) + + try: + frame = clip.load_frame() + + assert frame is not None + assert frame.shape == (6, 8, 3) + assert clip.i == 1 + + # Compression can slightly alter values, so verify channel ordering by relative values. + assert ( + frame[..., 0].mean() < frame[..., 1].mean() < frame[..., 2].mean() + or frame[..., 0].mean() != frame[..., 2].mean() + ) + finally: + clip.close() + + +def test_video_processor_cv_load_frame_eof_does_not_increment_counter(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=1, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path)) + + try: + assert clip.load_frame() is not None + assert clip.i == 1 + + eof_frame = clip.load_frame() + assert eof_frame is None + assert clip.i == 1 + finally: + clip.close() + + +def test_video_processor_cv_respects_nframes_cap(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=5, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path), nframes=2) + + try: + assert clip.nframes == 2 + finally: + clip.close() + + +def test_video_processor_cv_nframes_minus_one_uses_all_frames(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=3, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path), nframes=-1) + + try: + assert clip.nframes == 3 + finally: + clip.close() + + +def test_video_processor_cv_fps_override(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=3, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path), fps=123.0) + + try: + assert clip.fps == 123.0 + finally: + clip.close() + + +def test_video_processor_cv_can_write_video_with_default_dimensions(tmp_path): + input_path = tmp_path / "input.mp4" + output_path = tmp_path / "output.mp4" + _make_test_video(input_path, nframes=2, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV( + fname=str(input_path), + sname=str(output_path), + codec="mp4v", + ) + + try: + frame = clip.load_frame() + assert frame is not None + clip.save_frame(frame) + finally: + clip.close() + + assert output_path.exists() + assert output_path.stat().st_size > 0 + + +def test_video_processor_cv_can_write_video_with_explicit_dimensions(tmp_path): + input_path = tmp_path / "input.mp4" + output_path = tmp_path / "output.mp4" + _make_test_video(input_path, nframes=2, width=10, height=8, fps=10.0) + + clip = VideoProcessorCV( + fname=str(input_path), + sname=str(output_path), + codec="mp4v", + sw=6, + sh=4, + ) + + try: + frame = clip.load_frame() + frame = frame[:4, :6] + clip.save_frame(frame) + finally: + clip.close() + + assert output_path.exists() + assert output_path.stat().st_size > 0 + + +def test_close_is_idempotent(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=1, width=8, height=6, fps=10.0) + + clip = VideoProcessorCV(fname=str(video_path)) + clip.close() + clip.close() + + +@pytest.mark.xfail(reason="For backwards compatibility, VideoProcessorCV does not raise on invalid input videos.") +def test_invalid_input_video_raises_file_not_found_or_io_error(tmp_path): + missing_path = tmp_path / "missing.mp4" + + with pytest.raises((FileNotFoundError, OSError)): + VideoProcessorCV(fname=str(missing_path)) + + +def test_processor_passes_filename_as_str(tmp_path): + video_path = tmp_path / "input.mp4" + _make_test_video(video_path, nframes=1, width=8, height=6, fps=10.0) + + # Should not raise even if a Path object is passed. + clip = VideoProcessorCV(fname=video_path) + + try: + assert clip.fname == str(video_path) + finally: + clip.close() diff --git a/testscript_cli.py b/testscript_cli.py index 319344625b..295a68f729 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. @@ -9,27 +8,24 @@ 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, subprocess, sys - - -def install(package): - subprocess.check_call([sys.executable, "-m", "pip", "install", package]) - +import os +import platform -install("tensorflow==1.13.1") +import numpy as np +import pandas as pd import deeplabcut as dlc +from deeplabcut.core.engine import Engine -from pathlib import Path -import pandas as pd -import numpy as np -import platform - +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" video = [ @@ -107,7 +103,7 @@ def install(package): videoname, "CollectedData_" + scorer + ".h5", ), - "df_with_missing", + key="df_with_missing", format="table", mode="w", ) @@ -118,33 +114,14 @@ def install(package): print("CREATING TRAININGSET") dlc.create_training_dataset( - path_config_file, net_type=net_type, augmenter_type=augmenter_type + path_config_file, + net_type=net_type, + augmenter_type=augmenter_type, + engine=engine, ) -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", -) - -DLC_config = dlc.auxiliaryfunctions.read_plainconfig(posefile) -DLC_config["save_iters"] = numiter -DLC_config["display_iters"] = 2 -DLC_config["multi_step"] = [[0.001, numiter]] - -print("CHANGING training parameters to end quickly!") -dlc.auxiliaryfunctions.write_plainconfig(posefile, DLC_config) - print("TRAIN") -dlc.train_network(path_config_file) +dlc.train_network(path_config_file, epochs=numiter, displayiters=2) print("EVALUATE") dlc.evaluate_network(path_config_file, plotting=True) @@ -168,7 +145,8 @@ def install(package): 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 @@ -192,5 +170,6 @@ def install(package): 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)." ) diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000000..20d1b29f8b --- /dev/null +++ b/tools/README.md @@ -0,0 +1,209 @@ +# Developer tools useful for maintaining the repository + +This document summarizes the developer tooling and workflows used in this repo. + +```bash +pip install -e . --group dev +``` + +--- + +## 1) Pre-commit (recommended) + +Enable the repository hooks locally: + +```bash +pre-commit install +``` + +Run on all files: + +Steering committee members may edit the `NOTICE.yml` to update the header. + +## 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: + +Please follow the instructions in `CONTRIBUTING.md` for contributing to the codebase, including running tests and pre-commit checks before opening a pull request. + +Run from the repository root. Update `NOTICE.yml` to change header content. + +--- + +## 4) Running tests locally + +### Run the full test suite + +```bash +pytest +``` + +### Run a specific test module or folder + +```bash +coverage run -m pytest +coverage report +``` + +## 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. + +### What it outputs + +The selector emits **orthogonal workflow lanes** plus structured selections: + +- `lanes`: which workflow lanes should run + - `skip`: skip test execution entirely (for lint-only changes) + - `docs`: run docs checks + - `fast`: run targeted pytest paths and optional functional scripts + - `full`: delegate to the full test workflow / matrix +- `pytest_paths`: list of pytest path arguments (JSON) +- `functional_scripts`: list of Python scripts to run (JSON) +- `provenance`: mapping from each selected test/script to the category rule(s) that selected it + +It also emits audit metadata: + +- `selected_workflows`: ordered list of enabled lanes (`skip`, `docs`, `fast`, `full`) +- `lane_reasons`: reasons for each enabled lane +- `diff_mode`: how the diff range was determined +- `reasons`: aggregate machine-readable reasons for the decision +- `changed_files`: files considered for the decision +- `schema_version`: output schema version + +### Rule configuration + +Routing rules are defined in `tools/test_selector_config.py`. + +That file contains: + +- reusable path predicate helpers such as `prefix(...)`, `suffix(...)`, `equals(...)`, `case_insensitive_match(...)`, and `all_of(...)` +- conservative `FULL_SUITE_TRIGGERS` +- `LINT_ONLY_FILES` +- validated `CATEGORY_RULES` built from the `CategoryRule` schema +- `CATEGORY_RULE_BY_NAME` for stable lookup of named rules such as `docs` + +The current refactor keeps the rule predicates simple and location-based while validating the rule structure at import time. + +### Run locally (no CI env required) + +> [!IMPORTANT] +> Requires `pydantic>=2,<3` + +Print the decision as JSON: + +```bash +python tools/test_selector.py --json +``` + +Write the decision report (`selection.json` and `decision.md`) under `tmp/test-selection/`: + +```bash +python tools/test_selector.py --report-dir tmp/test-selection --json +``` + +Write a GitHub job-summary-compatible Markdown report when `GITHUB_STEP_SUMMARY` is available: + +```bash +python tools/test_selector.py --report-dir tmp/test-selection --write-summary +``` + +Override the diff range manually: + +```bash +python tools/test_selector.py --base-sha --head-sha --json +``` + +In GitHub Actions, the workflow typically adds `--write-github-output` and `--write-summary`. + +### Diff modes + +The selector records how the diff was determined in `diff_mode`: + +- `pr`: pull request diff using `merge-base(base, head)..head` +- `push`: push diff using `before..after` +- `manual`: explicit `--base-sha` / `--head-sha` +- `fallback`: fallback to `HEAD^..HEAD` +- `initial`: initial commit (`empty-tree..HEAD`) +- `fallback_no_head`: could not resolve `HEAD` + +### Report files + +The selector always writes report artifacts for transparency: + +- `tmp/test-selection/selection.json`: machine-readable output +- `tmp/test-selection/decision.md`: human-readable summary with workflow lanes, reasons, explained changed files, selected tests, and provenance + +These reports are especially useful when a change unexpectedly routes to `full`. + +### Notes + +- The selector can enable more than one lane at once. For example, a PR can legitimately enable both `docs` and `fast`, or `docs` and `full`. +- Docs changes are **orthogonal** to test routing: docs changes can enable the docs lane while still contributing selected tests/scripts if such rules are configured. +- `LINT_ONLY_FILES` are ignored for routing. If *only* lint-only files changed, the selector enables the `skip` lane. +- If category rules match changed files but do not contribute explicit tests/scripts, the selector can fall back to the minimal pytest set defined by `MINIMAL_PYTEST`. + +### Troubleshooting the selector + +If a workflow run is unexpectedly selecting `full`, check: + +- `tmp/test-selection/decision.md` +- `tmp/test-selection/selection.json` +- `lane_reasons` +- `diff_mode` +- `changed_files` + +Common causes include: + +- a file matched a conservative full-suite trigger +- no category rule matched the routed files +- selected paths configured by a rule no longer exist in the repository +- diff resolution fell back because CI checkout history was incomplete + +--- + +## 6) Docs: Jupyter Book build (local) + +The repo uses Jupyter Book for docs: + +```bash +python -m pip install -U pip +python -m pip install .[docs] +jupyter-book build . +``` + +`.github/workflows/build-book.yml` is the canonical CI implementation. + +--- + +## 7) Testing the test selector + +The selector has dedicated tests covering: + +- decision behavior for docs / fast / full / skip routing +- provenance and deduplicated selections +- `CategoryRule` schema validation +- integrity checks for the currently defined rules + +Run the selector-focused tests with: + +```bash +pytest tests/tools/test_selector/ +``` + +--- + +## 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: + - broaden the fast-lane install (for example by installing required extras), or + - adjust selection rules so that the fast lane only selects tests that run in the minimal environment. +- If manual diff selection is used, always pass both `--base-sha` and `--head-sha` together. +- In CI, ensure checkout history is deep enough for `merge-base` / `diff` operations (`fetch-depth: 0` is typically safest). diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/docs_and_notebooks_audit.py b/tools/docs_and_notebooks_audit.py new file mode 100644 index 0000000000..e4dbc0b861 --- /dev/null +++ b/tools/docs_and_notebooks_audit.py @@ -0,0 +1,581 @@ +""" +DeepLabCut docs audit export tool (validated / extensible). + +Purpose +------- +Read audit metadata from the `deeplabcut` namespace in Markdown frontmatter and +notebook-level metadata, validate selected fields against enums/schema, and +export a CSV register with docs metadata and review notes to help drive documentation maintenance. + +Supported metadata fields +------------------------- +From `deeplabcut:` this tool currently validates and exports: +- visibility +- status +- recommendation (with fallback alias: review_decision) +- last_verified (pass-through) +- notes (pass-through, but preserved from previous CSV if present even if updated in source) + +Example metadata +---------------- +Markdown frontmatter: + +--- +deeplabcut: + visibility: online + status: viable + recommendation: keep + last_verified: 2026-04-15 +--- + +Notebook metadata: +{ + "metadata": { + "deeplabcut": { + "visibility": "online", + "status": "viable", + "recommendation": "keep", + "last_verified": "2026-04-15" + } + } +} + +Usage +----- +python tools/docs_and_notebooks_audit.py \ + --config tools/docs_and_notebooks_report_config.yml \ + --out docs/_meta/docs_audit_register.csv + +python tools/docs_and_notebooks_audit.py \ + --targets docs/gui/ docs/recipes/*.md examples/COLAB/*.ipynb +""" + +from __future__ import annotations + +import argparse +import csv +import fnmatch +import os +import re +import subprocess +from collections.abc import Callable, Iterable, Sequence +from enum import Enum +from pathlib import Path +from typing import Any, Literal, TypedDict + +import nbformat +import yaml +from pydantic import BaseModel, ConfigDict, ValidationError + +# ----------------------------------------------------------------------------- +# Constants / defaults +# ----------------------------------------------------------------------------- +DLC_NAMESPACE = "deeplabcut" +DEFAULT_CONFIG = Path("tools/docs_and_notebooks_report_config.yml") +DEFAULT_OUTPUT = Path("docs/_meta/docs_audit_register.csv") +GLOB_CHARS = set("*?[") +FRONTMATTER_RE = re.compile(r"^---\s*$") + +# Generated columns owned by this tool. Any additional columns present in an +# existing CSV are preserved as human columns. +GENERATED_COLUMNS = [ + "path", + "kind", + "metadata_present", + "visibility", + "status", + "recommendation", + "last_verified", + "parse_error", + "validation_error", + "notes", +] + +DEFAULT_INCLUDE = [ + "docs/**/*.md", + "docs/**/*.markdown", + "docs/**/*.ipynb", + "examples/**/*.ipynb", + "README.md", + "CONTRIBUTING.md", +] +DEFAULT_EXCLUDE = [ + ".git/**", + ".github/**", + "**/.ipynb_checkpoints/**", + "**/node_modules/**", + "**/.venv/**", +] + +FileKind = Literal["md", "ipynb", "other"] +TargetKind = Literal["invalid", "file", "dir", "glob"] + + +# ----------------------------------------------------------------------------- +# Enums / schema +# ----------------------------------------------------------------------------- +class Visibility(str, Enum): + """How discoverable the page is in the documentation surface.""" + + ONLINE = "online" # current docs surface / discoverable + UNLISTED = "unlisted" # intentionally available but not surfaced in nav + ARCHIVED = "archived" # only discoverable via archive/historical area + ORPHANED = "orphaned" # not listed and no supported inbound links + + +class Status(str, Enum): + """Current health / lifecycle state of the page.""" + + VIABLE = "viable" # current and acceptable + REVIEW_NEEDED = "review_needed" # needs human review before decision + OUTDATED = "outdated" # content exists but is stale / drifted + DEPRECATED = "deprecated" # not preferred; replacement exists/coming + ARCHIVED = "archived" # kept for historical or niche reference + REMOVED = "removed" # removed from active docs surface + + +class Recommendation(str, Enum): + """Recommended next action for the page.""" + + KEEP = "keep" # content is fine as-is; no action needed + VERIFY = "verify" # content and/or formatting could use verification + UPDATE = "update" # requires content update to be considered viable + MOVE = "move" # move to a more appropriate location + MERGE = "merge" # merge into another page + ARCHIVE = "archive" # if deprecated, archive before removal + REMOVE = "remove" # remove from repository (can be resurrected from git history if needed) + + +class AuditMetadata(BaseModel): + """ + Strictly validate only the fields this exporter owns. + + Keep extra metadata allowed so the deeplabcut namespace can still contain + other fields used by the main checks tool or future workflows. + """ + + model_config = ConfigDict(extra="allow") + + visibility: Visibility | None = None + status: Status | None = None + recommendation: Recommendation | None = None + last_verified: str | None = None + notes: str | None = None + + +class TargetSpec(TypedDict): + raw: str + normalized: str + kind: TargetKind + + +class FieldSpec(BaseModel): + """Describes how a CSV column maps from deeplabcut metadata.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + column: str + source_keys: list[str] + extractor: Callable[[AuditMetadata, dict[str, Any]], str] | None = None + + +FIELD_SPECS: list[FieldSpec] = [ + FieldSpec(column="visibility", source_keys=["visibility"]), + FieldSpec(column="status", source_keys=["status"]), + FieldSpec(column="recommendation", source_keys=["recommendation", "review_decision"]), + FieldSpec(column="last_verified", source_keys=["last_verified"]), + FieldSpec(column="notes", source_keys=["notes"]), +] + + +# ----------------------------------------------------------------------------- +# Path / target helpers +# ----------------------------------------------------------------------------- +def normalize_target_spec(spec: str, repo_root: Path) -> str: + s = spec.strip() + if not s: + return s + s = s.replace("\\", "/") + while s.startswith("./"): + s = s[2:] + p = Path(s) + if p.is_absolute(): + try: + s = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + except ValueError: + s = str(p).replace(os.sep, "/") + s = re.sub(r"/+", "/", s) + if len(s) > 1: + s = s.rstrip("/") + return s + + +def compile_target_specs(targets: list[str] | None, repo_root: Path) -> list[TargetSpec] | None: + if not targets: + return None + specs: list[TargetSpec] = [] + for raw in targets: + normalized = normalize_target_spec(raw, repo_root) + if not normalized: + specs.append({"raw": raw, "normalized": "", "kind": "invalid"}) + continue + if any(ch in normalized for ch in GLOB_CHARS): + specs.append({"raw": raw, "normalized": normalized, "kind": "glob"}) + continue + if raw.endswith(("/", "\\")): + specs.append({"raw": raw, "normalized": normalized, "kind": "dir"}) + continue + candidate = repo_root / normalized + specs.append( + { + "raw": raw, + "normalized": normalized, + "kind": "dir" if candidate.exists() and candidate.is_dir() else "file", + } + ) + return specs + + +def target_spec_matches_path(rel_path: str, spec: TargetSpec) -> bool: + rel_path = rel_path.replace("\\", "/") + kind = spec["kind"] + normalized = spec["normalized"] + if kind == "invalid": + return False + if kind == "file": + return rel_path == normalized + if kind == "dir": + return rel_path == normalized or rel_path.startswith(normalized + "/") + if kind == "glob": + return fnmatch.fnmatchcase(rel_path, normalized) + return False + + +def target_matches(rel_path: str, specs: list[TargetSpec] | None) -> bool: + return True if specs is None else any(target_spec_matches_path(rel_path, spec) for spec in specs) + + +def find_repo_root(start: Path) -> Path: + cur = start.resolve() + for _ in range(50): + if (cur / ".git").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + proc = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=str(start), capture_output=True, text=True) + if proc.returncode == 0 and proc.stdout.strip(): + return Path(proc.stdout.strip()).resolve() + raise RuntimeError("Could not locate repository root") + + +def file_kind(path: Path) -> FileKind: + suffix = path.suffix.lower() + if suffix in {".md", ".markdown"}: + return "md" + if suffix == ".ipynb": + return "ipynb" + return "other" + + +# ----------------------------------------------------------------------------- +# Config / discovery +# ----------------------------------------------------------------------------- +def load_scan_patterns(config_path: Path | None) -> tuple[list[str], list[str]]: + if not config_path or not config_path.exists(): + return DEFAULT_INCLUDE, DEFAULT_EXCLUDE + + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + scan = raw.get("scan") or {} + include = scan.get("include") or DEFAULT_INCLUDE + exclude = scan.get("exclude") or DEFAULT_EXCLUDE + return include, exclude + + +def is_excluded(rel_path: str, exclude_patterns: list[str]) -> bool: + return any(fnmatch.fnmatch(rel_path, pat) for pat in exclude_patterns) + + +def iter_candidate_paths( + repo_root: Path, + include_patterns: list[str], + exclude_patterns: list[str], + targets: list[str] | None = None, +) -> list[Path]: + specs = compile_target_specs(targets, repo_root) + matches: dict[str, Path] = {} + for pattern in include_patterns: + for path in repo_root.glob(pattern): + if not path.is_file(): + continue + rel = str(path.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, exclude_patterns): + continue + if not target_matches(rel, specs): + continue + matches[rel] = path.resolve() + return [matches[k] for k in sorted(matches)] + + +# ----------------------------------------------------------------------------- +# Metadata readers (minimal duplication via namespace dispatch) +# ----------------------------------------------------------------------------- +def read_md_frontmatter(text: str) -> tuple[dict | None, str | None]: + lines = text.splitlines(keepends=True) + if not lines or not FRONTMATTER_RE.match(lines[0]): + return None, None + + end_idx = None + for i in range(1, min(len(lines), 5000)): + if FRONTMATTER_RE.match(lines[i]): + end_idx = i + break + if end_idx is None: + return None, "unterminated_markdown_frontmatter" + + fm_text = "".join(lines[1:end_idx]) + try: + fm = yaml.safe_load(fm_text) if fm_text.strip() else {} + except Exception as exc: + return None, f"markdown_frontmatter_yaml_error: {exc}" + if not isinstance(fm, dict): + return None, "markdown_frontmatter_not_mapping" + return fm, None + + +def read_container(path: Path) -> tuple[dict | None, str | None]: + """ + Return the top-level metadata container for a file. + - Markdown: YAML frontmatter mapping + - Notebook: notebook.metadata mapping + """ + kind = file_kind(path) + + if kind == "md": + try: + text = path.read_text(encoding="utf-8") + except Exception as exc: + return None, f"read_failed: {exc}" + return read_md_frontmatter(text) + + if kind == "ipynb": + try: + nb = nbformat.read(str(path), as_version=4) + meta = getattr(nb, "metadata", {}) or {} + except Exception as exc: + return None, f"notebook_read_failed: {exc}" + if not isinstance(meta, dict): + return None, "notebook_metadata_not_mapping" + return meta, None + + return None, None + + +def read_dlc_namespace(path: Path) -> tuple[dict | None, str | None]: + container, error = read_container(path) + if error: + return None, error + if container is None: + return None, None + raw = container.get(DLC_NAMESPACE) + if raw is None: + return None, None + if not isinstance(raw, dict): + return None, "deeplabcut_namespace_not_mapping" + return raw, None + + +# ----------------------------------------------------------------------------- +# Validation / normalization +# ----------------------------------------------------------------------------- +def build_validation_input(raw_meta: dict[str, Any]) -> dict[str, Any]: + """ + Map raw deeplabcut metadata into the schema-owned keys. + This is where aliases are resolved to canonical names. + """ + payload: dict[str, Any] = {} + for spec in FIELD_SPECS: + for key in spec.source_keys: + if key in raw_meta and raw_meta.get(key) not in {None, ""}: + payload[spec.column] = raw_meta.get(key) + break + return payload + + +def validate_metadata(raw_meta: dict[str, Any] | None) -> tuple[AuditMetadata | None, str | None]: + if raw_meta is None: + return None, None + try: + validated = AuditMetadata.model_validate(build_validation_input(raw_meta)) + return validated, None + except ValidationError as exc: + messages = [] + for err in exc.errors(): + loc = ".".join(str(x) for x in err.get("loc", [])) + msg = err.get("msg", "invalid value") + messages.append(f"{loc}: {msg}" if loc else msg) + return None, "; ".join(messages) + + +def extract_field_value(spec: FieldSpec, validated: AuditMetadata | None, raw_meta: dict[str, Any]) -> str: + if spec.extractor is not None: + return spec.extractor(validated, raw_meta) + if validated is not None: + value = getattr(validated, spec.column, None) + if isinstance(value, Enum): + return value.value + return "" if value is None else str(value) + # If validation failed, still emit raw/aliased value when present for easier triage. + for key in spec.source_keys: + if key in raw_meta and raw_meta.get(key) is not None: + return str(raw_meta.get(key)) + return "" + + +# ----------------------------------------------------------------------------- +# CSV merge / preserve human annotations +# ----------------------------------------------------------------------------- +def load_existing_rows(csv_path: Path) -> tuple[dict[str, dict[str, str]], list[str]]: + if not csv_path.exists(): + return {}, [] + with csv_path.open("r", newline="", encoding="utf-8") as fh: + reader = csv.DictReader(fh) + rows = {row.get("path", ""): row for row in reader if row.get("path")} + existing_columns = reader.fieldnames or [] + extra_columns = [c for c in existing_columns if c not in GENERATED_COLUMNS] + return rows, extra_columns + + +def merged_row( + base: dict[str, Any], + previous: dict[str, str] | None, + extra_columns: Iterable[str], + force_overwrite_notes: bool = False, +) -> dict[str, Any]: + row = dict(base) + + if previous: + prev_notes = (previous.get("notes") or "").strip() + scanned_notes = (row.get("notes") or "").strip() + + if prev_notes and scanned_notes and prev_notes != scanned_notes: + print(f"WARNING: Notes conflict for {row['path']}:") + print(f"- Previous: {prev_notes}") + print(f"- Scanned: {scanned_notes}") + + if force_overwrite_notes: + print("Force overwrite enabled; using scanned notes.") + else: + print("Preserving previous notes and ignoring scanned notes.") + + if force_overwrite_notes: + row["notes"] = scanned_notes + else: + row["notes"] = prev_notes if prev_notes else scanned_notes + + for col in extra_columns: + row[col] = previous.get(col, "") if previous else "" + + return row + + +# ----------------------------------------------------------------------------- +# Row building / export +# ----------------------------------------------------------------------------- +def build_row(repo_root: Path, path: Path) -> dict[str, Any]: + rel = str(path.resolve().relative_to(repo_root)).replace(os.sep, "/") + kind = file_kind(path) + raw_meta, parse_error = read_dlc_namespace(path) + raw_meta = raw_meta or {} + metadata_present = bool(raw_meta) + validated, validation_error = validate_metadata(raw_meta if metadata_present else None) + + row: dict[str, Any] = { + "path": rel, + "kind": kind, + "metadata_present": "true" if metadata_present else "false", + "parse_error": parse_error or "", + "validation_error": validation_error or "", + # "notes": "", + } + + for spec in FIELD_SPECS: + row[spec.column] = extract_field_value(spec, validated, raw_meta) + return row + + +def export_csv( + repo_root: Path, + include: list[str], + exclude: list[str], + out_path: Path, + targets: list[str] | None, + force_overwrite_notes: bool = False, +) -> int: + candidates = iter_candidate_paths(repo_root, include, exclude, targets=targets) + existing_rows, extra_columns = load_existing_rows(out_path) + + rows = [] + for path in candidates: + base = build_row(repo_root, path) + previous = existing_rows.get(base["path"]) + rows.append(merged_row(base, previous, extra_columns, force_overwrite_notes=force_overwrite_notes)) + + fieldnames = list(GENERATED_COLUMNS) + [c for c in extra_columns if c not in GENERATED_COLUMNS] + + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", newline="", encoding="utf-8") as fh: + writer = csv.DictWriter(fh, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + writer.writerow(row) + + print(f"Wrote {len(rows)} records to {out_path}") + invalid = sum(1 for row in rows if row.get("validation_error")) + parse_fail = sum(1 for row in rows if row.get("parse_error")) + if invalid or parse_fail: + print(f"Validation issues: {invalid}; parse issues: {parse_fail}") + return 0 + + +# ----------------------------------------------------------------------------- +# CLI +# ----------------------------------------------------------------------------- +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Export DeepLabCut audit metadata to CSV") + parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Optional path to scan config YAML") + parser.add_argument("--root", default=".", help="Repository root or path inside the repository") + parser.add_argument("--out", default=str(DEFAULT_OUTPUT), help="CSV output path") + parser.add_argument( + "--targets", + nargs="*", + help=( + "Optional repo-relative targets to limit the export. Supports exact files, " + "directories, and glob patterns (e.g. docs/page.md, docs/gui/, 'docs/**/*.md')." + ), + ) + parser.add_argument( + "--force-overwrite-notes", + action="store_true", + help=( + "By default, if a record already exists in the CSV and has notes, those notes are preserved even if the " + "scanned metadata contains notes. This flag forces the scanned notes to overwrite existing notes, " + "which can be useful for bulk updates but may lead to loss of manually curated information." + ), + ) + args = parser.parse_args(list(argv) if argv is not None else None) + + repo_root = find_repo_root(Path(args.root)) + config_path = Path(args.config) + include, exclude = load_scan_patterns(config_path) + out_path = Path(args.out) + if not out_path.is_absolute(): + out_path = repo_root / out_path + + return export_csv( + repo_root, include, exclude, out_path, targets=args.targets, force_overwrite_notes=args.force_overwrite_notes + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py new file mode 100644 index 0000000000..d7d9690e8c --- /dev/null +++ b/tools/docs_and_notebooks_check.py @@ -0,0 +1,1388 @@ +"""DeepLabCut docs & notebooks automated checks tool. + +Goals +----- +- SAFE by default: read-only operations in CI (report/check). +- Idempotent updates (update mode) that only touch: + * Notebook-level metadata for .ipynb (never cells/outputs) + * YAML frontmatter for .md docs (optional) +- Uses pydantic schemas with explicit schema_version for validation. +- Aims to be contributor-friendly by default: check mode enforces configured policy, while + surfacing scan/parsing issues without failing unless strict mode is enabled. + +Terminology +----------- +last_content_updated + Computed from git history, excluding metadata-only commits. + (Metadata commits must include META_COMMIT_MARKER in the commit message.) + +last_verified + Human-controlled date indicating the file was verified to work/be accurate. + +verified_for + Human-controlled string, typically the project version (e.g. 3.0.0rc13). + +tier + Optional classification (left unset by default; do not auto-populate). + +Usage modes +----------- +Report (read-only): + python tools/docs_and_notebooks_check.py report + +Check (read-only; policy enforcement): + python tools/docs_and_notebooks_check.py check + + Runs scans and evaluates configured policy rules. + Exits non-zero for policy violations. + Scan/parsing errors are always reported in console / JSON / Markdown output, + but are non-fatal by default unless strict mode is enabled or they imply a + policy violation. + +Update content-date field from git (write mode; requires --write): + python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git + +Update verification fields for selected targets (write mode): + python tools/docs_and_notebooks_check.py update --write --targets docs/page.md \ + --set-last-verified today --set-verified-for 3.0.0rc13 + +Normalize notebooks deterministically (explicit churn; write mode): + python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb + + +Configuration +------------- +Uses tools/docs_and_notebooks_report_config.yml by default. + +Outputs +------- +- docs_nb_checks.json: machine-readable report +- docs_nb_checks.md: human-readable summary + +Notes for CI +------------ +- Ensure actions/checkout uses fetch-depth: 0 (or sufficiently deep), + otherwise git log may not see history. +- Requires: + - pydantic>=2,<3 + - 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. +""" + +# tools/docs_and_notebooks_check.py +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import re +import shlex +import subprocess +from collections.abc import Sequence +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Literal, TypedDict + +import nbformat +import yaml +from nbformat.validator import NotebookValidationError +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +REPORT_SCHEMA_VERSION: Literal[1, 2] = 2 +GLOB_CHARS = set("*?[") +DLC_NAMESPACE = "deeplabcut" +OUTPUT_FILENAME = "docs_nb_checks" +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_CFG = SCRIPT_DIR / "docs_and_notebooks_report_config.yml" + + +# ----------------------------- +# Metadata commit marker / guidance +# ----------------------------- +# IMPORTANT: +# Metadata-only updates and notebook normalization rewrite files and will change +# "git last touched" timestamps. To preserve meaningful "content age", all such +# commits must include this marker in the commit message. +META_COMMIT_MARKER = "chore(metadata)" +SUGGESTED_TAGGED_COMMIT = f"{META_COMMIT_MARKER}: update docs/notebooks metadata" + + +# ----------------------------- +# Pydantic schemas +# ----------------------------- + + +class DLCMeta(BaseModel): + """Metadata embedded in files under the `deeplabcut` namespace.""" + + model_config = ConfigDict(extra="allow") + + # Tool-managed: last meaningful content update date (excluding metadata commits) + last_content_updated: date | None = None + + # Optional tool-managed: last time metadata/normalization was performed + last_metadata_updated: date | None = None + # Optional human-managed verification fields + last_verified: date | None = None + # Version or other string indicating what this file was verified for (e.g. "3.0.0rc13") + verified_for: str | None = None + # Extra metadata fields for later usage (e.g. allowlist tier classification), but not currently used by the tool + tier: str | None = None + ignore: bool = False + notes: str | None = None + + +class ScanConfig(BaseModel): + include: list[str] = Field(default_factory=list) + exclude: list[str] = Field(default_factory=list) + + +class PolicyConfig(BaseModel): + warn_if_content_older_than_days: int = 365 + warn_if_verified_older_than_days: int = 365 + missing_last_verified_is_warning: bool = True + + # Strict-mode toggle: if true, scan/parsing errors also fail `check` + fail_on_scan_errors: bool = False + fail_if_metadata_sync_needed: 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_notebook_normalized: list[str] = Field(default_factory=list) + + +class ToolConfig(BaseModel): + version: Literal[1] = 1 + scan: ScanConfig + policy: PolicyConfig + + +FileKind = Literal["ipynb", "md", "other"] + + +class FileRecord(BaseModel): + path: str + kind: FileKind + + # Computed from git (excluding metadata-only commits) + last_content_updated: date | None = None + # Debug-only: raw git last touched (may be metadata commit) + last_git_touched: date | None = None + + # Read from file metadata/frontmatter + meta: DLCMeta | None = None + + # Derived + 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) + + # If update mode would change file + would_change: bool = False + + +class Report(BaseModel): + schema_version: Literal[1, 2] = REPORT_SCHEMA_VERSION + generated_at: datetime + repo_root: str + config_path: str + + totals: dict[str, int] + records: list[FileRecord] + + # New: actionable metadata-sync guidance for maintainers + metadata_sync_targets: list[str] = Field(default_factory=list) + metadata_sync_command: str | None = None + + +# Rebuild models due to __future__ annotations +DLCMeta.model_rebuild() +ScanConfig.model_rebuild() +PolicyConfig.model_rebuild() +ToolConfig.model_rebuild() +FileRecord.model_rebuild() +Report.model_rebuild() + +TargetKind = Literal["invalid", "file", "dir", "glob"] + + +class TargetSpec(TypedDict): + raw: str + normalized: str + kind: TargetKind + + +# ----------------------------- +# Helpers +# ----------------------------- +def normalize_target_spec(spec: str, repo_root: Path) -> str: + """ + Normalize a CLI target into a repo-relative POSIX-style path/pattern. + + Examples + -------- + .\\docs\\a.md -> docs/a.md + ./docs/a.md -> docs/a.md + docs\\gui\\ -> docs/gui + /abs/path/in/repo/a.md -> docs/a.md (if inside repo) + """ + s = spec.strip() + if not s: + return s + + # Normalize slashes first so Windows-style input works everywhere + s = s.replace("\\", "/") + + # Strip leading ./ repeatedly + while s.startswith("./"): + s = s[2:] + + # If absolute and inside repo, make it repo-relative + p = Path(s) + if p.is_absolute(): + try: + s = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + except ValueError: + # Outside repo: keep normalized absolute text so it can fail validation cleanly + s = str(p).replace(os.sep, "/") + + # Collapse repeated slashes + s = re.sub(r"/+", "/", s) + + # Remove trailing slash for canonical matching + if len(s) > 1: + s = s.rstrip("/") + + return s + + +def compile_target_specs(targets: list[str] | None, repo_root: Path) -> list[TargetSpec] | None: + """ + Convert raw CLI targets into normalized selector specs. + + Each spec is a dict with: + - raw: original user input + - normalized: normalized repo-relative selector + - kind: file | dir | glob | invalid + """ + if not targets: + return None + + specs: list[TargetSpec] = [] + + for raw in targets: + normalized = normalize_target_spec(raw, repo_root) + if not normalized: + specs.append({"raw": raw, "normalized": "", "kind": "invalid"}) + continue + + if any(ch in normalized for ch in GLOB_CHARS): + specs.append({"raw": raw, "normalized": normalized, "kind": "glob"}) + continue + + # Treat explicit trailing slash/backslash as directory intent + if raw.endswith(("/", "\\")): + specs.append({"raw": raw, "normalized": normalized, "kind": "dir"}) + continue + + candidate = Path(normalized) + abs_candidate = candidate if candidate.is_absolute() else (repo_root / candidate) + if abs_candidate.exists() and abs_candidate.is_dir(): + specs.append({"raw": raw, "normalized": normalized, "kind": "dir"}) + else: + specs.append({"raw": raw, "normalized": normalized, "kind": "file"}) + + return specs + + +def target_spec_matches_path(rel_path: str, spec: TargetSpec) -> bool: + rel_path = rel_path.replace("\\", "/") + + kind = spec["kind"] + normalized = spec["normalized"] + + if kind == "invalid": + return False + + if kind == "file": + return rel_path == normalized + + if kind == "dir": + return rel_path == normalized or rel_path.startswith(normalized + "/") + + if kind == "glob": + # Intentionally use simple shell-style matching here so patterns like + # docs/**/*.md behave the way users generally expect across platforms. + return fnmatch.fnmatchcase(rel_path, normalized) + + return False + + +def target_matches(rel_path: str, specs: list[TargetSpec] | None) -> bool: + if specs is None: + return True + return any(target_spec_matches_path(rel_path, spec) for spec in specs) + + +def iter_scan_candidate_paths(repo_root: Path, cfg: ToolConfig) -> list[str]: + """ + Return all repo-relative paths that are in scope for scanning, before applying --targets. + """ + rels: list[str] = [] + for p in glob_paths(repo_root, cfg.scan.include): + rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, cfg.scan.exclude): + continue + rels.append(rel) + return sorted(set(rels)) + + +def validate_requested_targets( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, +) -> tuple[list[str], list[str]]: + """ + Validate CLI target selectors against the current scan universe. + + Returns: + matched_paths: repo-relative file paths matched by any selector + unmatched_targets: raw target strings that matched nothing + """ + if not targets: + return [], [] + + specs = compile_target_specs(targets, repo_root) + candidates = iter_scan_candidate_paths(repo_root, cfg) + + matched_paths = sorted({rel for rel in candidates if target_matches(rel, specs)}) + + unmatched_targets: list[str] = [] + for spec in specs or []: + if spec["kind"] == "invalid": + unmatched_targets.append(spec["raw"]) + elif not any(target_spec_matches_path(rel, spec) for rel in candidates): + unmatched_targets.append(spec["raw"]) + + return matched_paths, unmatched_targets + + +def print_target_match_summary(matched_paths: list[str], requested_targets: list[str] | None) -> None: + """ + Print a small CLI summary whenever --targets is used. + """ + if not requested_targets: + return + + print(f"\nMatched {len(matched_paths)} file(s) from --targets:") + preview_limit = 50 + for rel in matched_paths[:preview_limit]: + print(f"- {rel}") + if len(matched_paths) > preview_limit: + print(f"... and {len(matched_paths) - preview_limit} more") + + +def _iso_today() -> date: + return datetime.now(timezone.utc).date() + + +def _run_git(args: Sequence[str], cwd: Path) -> tuple[int, str, str]: + p = subprocess.run( + ["git", *args], + cwd=str(cwd), + capture_output=True, + text=True, + ) + return p.returncode, p.stdout.strip(), p.stderr.strip() + + +def find_repo_root(start: Path) -> Path: + cur = start.resolve() + for _ in range(50): + if (cur / ".git").exists(): + return cur + if cur.parent == cur: + break + cur = cur.parent + code, out, _err = _run_git(["rev-parse", "--show-toplevel"], cwd=start) + if code == 0 and out: + return Path(out).resolve() + raise RuntimeError("Could not locate repository root") + + +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: + return any(fnmatch.fnmatch(rel_path, pat) for pat in exclude_patterns) + + +def file_kind(path: Path) -> str: + s = path.suffix.lower() + if s == ".ipynb": + return "ipynb" + if s in {".md", ".markdown"}: + return "md" + return "other" + + +def _parse_git_iso_date(out: str) -> date | None: + out = (out or "").strip() + if not out: + return None + + try: + return date.fromisoformat(out) + except Exception: + pass + + try: + if out.endswith("Z"): + out = out[:-1] + "+00:00" + return datetime.fromisoformat(out).date() + except Exception: + return None + + +def _git_log_date(repo_root: Path, rel_path: str, extra_args: Sequence[str] = ()) -> date | None: + args = [ + "log", + "-1", + "--date=short", + "--format=%cd", + *extra_args, + "--", + rel_path, + ] + code, out, _err = _run_git(args, cwd=repo_root) + if code != 0: + return None + return _parse_git_iso_date(out) + + +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[date | None, bool]: + d = _git_log_date( + repo_root, + rel_path, + extra_args=[ + "--fixed-strings", + "--invert-grep", + "--grep", + META_COMMIT_MARKER, + ], + ) + if d is not None: + return d, False + return git_last_touched(repo_root, rel_path), True + + +FRONTMATTER_RE = re.compile(r"^---\s*$") + + +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 + + end_idx = None + for i in range(1, min(len(lines), 5000)): + if FRONTMATTER_RE.match(lines[i]): + end_idx = i + break + + if end_idx is None: + return None, text, "unterminated_markdown_frontmatter" + + fm_text = "".join(lines[1:end_idx]) + body = "".join(lines[end_idx + 1 :]) + + if yaml is None: + raise RuntimeError("PyYAML is required to parse Markdown frontmatter") + + fm = yaml.safe_load(fm_text) if fm_text.strip() else {} + if not isinstance(fm, dict): + return None, text, "markdown_frontmatter_not_mapping" + + return fm, body, None + + +def dump_md_frontmatter(frontmatter: dict, body: str) -> str: + if yaml is None: + raise RuntimeError("PyYAML is required to write Markdown frontmatter") + fm_text = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True) + body_to_write = body + if body_to_write.startswith("\n"): + body_to_write = body_to_write[1:] + return "---\n" + fm_text + "---\n" + body_to_write + + +def read_ipynb_meta(path: Path) -> tuple[Any, dict, bool]: + """ + Read a notebook using nbformat. + Returns (notebook_node, deeplabcut_meta_dict, has_dlc_namespace). + """ + nb = nbformat.read(str(path), as_version=4) + + meta = getattr(nb, "metadata", {}) or {} + has_dlc = DLC_NAMESPACE in meta + + raw_dlc_meta = meta.get(DLC_NAMESPACE) + return nb, raw_dlc_meta, has_dlc + + +def notebook_is_normalized(path: Path, nb: Any) -> bool: + original = path.read_text(encoding="utf-8") + # Normalize newline style so CRLF vs LF differences do not cause false mismatches + original_normalized = original.replace("\r\n", "\n").replace("\r", "\n") + normalized = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + "\n" + return original_normalized == normalized + + +def write_ipynb_meta(path: Path, nb: Any) -> None: + """ + Write a notebook using nbformat. + + Note: nbformat writes JSON in a canonical form; it *will* rewrite the file, + so expect diffs if the notebook wasn't previously normalized to the same style. + """ + # Validate before writing (optional but recommended) + nbformat.validate(nb) + + # Use a stable indentation to reduce churn (choose 2 if your repo tends that way) + text = nbformat.writes(nb, version=4, indent=2, ensure_ascii=False) + + path.write_text(text + "\n", encoding="utf-8") + + +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 + try: + return DLCMeta.model_validate(raw), True + except ValidationError: + return None, False + + +def meta_to_jsonable(meta: DLCMeta) -> dict: + """ + Return JSON-serializable metadata (dates become ISO strings). + This prevents json.dumps() from failing when writing .ipynb files. + """ + return meta.model_dump(mode="json", exclude_none=True) + + +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: + # Support exact matches or glob patterns + return any(pat == rel_path or fnmatch.fnmatch(rel_path, pat) for pat in allowlist) + + +# ----------------------------- +# CI enforcement utils +# ----------------------------- +def record_needs_metadata_sync(rec: FileRecord) -> bool: + if "invalid_metadata" in rec.warnings: + # avoid mixing "invalid metadata" issues with "metadata sync needed" guidance, + # since the former may require manual fixes + return False + + # If metadata could not be read/parsed/validated reliably, just return False. + # These are scan/repair issues first, not "missing/out-of-sync metadata". + metadata_sync_blocking_error_prefixes = ( + "metadata_read_failed:", + "markdown_frontmatter_invalid:", + "nbformat_invalid:", + ) + if any(err.startswith(metadata_sync_blocking_error_prefixes) for err in (rec.errors or [])): + return False + + if rec.kind not in {"md", "ipynb"}: + return False + if rec.meta and rec.meta.ignore: + return False + + embedded = rec.meta.last_content_updated if rec.meta else None + computed = rec.last_content_updated + + if computed is None: + return False + + return embedded != computed + + +def collect_metadata_sync_targets(records: list[FileRecord]) -> list[str]: + paths: list[str] = [] + for rec in records: + if record_needs_metadata_sync(rec): + paths.append(rec.path) + return sorted(set(paths)) + + +def build_metadata_sync_command(config_path: str, paths: list[str]) -> str | None: + if not paths: + return None + config_path = shlex.quote(config_path) + paths = [shlex.quote(p) for p in paths] + + target_lines = " \\\n ".join(paths) + return ( + "python tools/docs_and_notebooks_check.py \\\n" + f" --config {config_path} \\\n" + " update \\\n" + " --write \\\n" + " --set-content-date-from-git \\\n" + " --targets \\\n" + f" {target_lines} \\\n" + " --ack-meta-commit-marker" + ) + + +def build_git_add_command(paths: list[str]) -> str | None: + if not paths: + return None + paths = [shlex.quote(p) for p in paths] + + path_lines = " \\\n ".join(paths) + return f"git add \\\n {path_lines}" + + +# ----------------------------- +# Core scanning +# ----------------------------- + + +def load_config(config_path: Path) -> ToolConfig: + if yaml is None: + raise RuntimeError("PyYAML is required (pip install pyyaml)") + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) + return ToolConfig.model_validate(raw) + + +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] = [] + target_specs = compile_target_specs(targets, repo_root) + + for p in paths: + rel = str(p.resolve().relative_to(repo_root)).replace(os.sep, "/") + if is_excluded(rel, cfg.scan.exclude): + continue + if not target_matches(rel, target_specs): + continue + kind = file_kind(p) + 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) + if used_fallback: + rec.warnings.append("content_date_fallback_to_git_touched") + + try: + if kind == "ipynb": + nb, raw_meta, has_dlc = read_ipynb_meta(p) + + try: + nbformat.validate(nb) + except NotebookValidationError as e: + rec.errors.append(f"nbformat_invalid: {e}") + + try: + if not notebook_is_normalized(p, nb): + rec.warnings.append("notebook_not_normalized") + except Exception as e: + rec.errors.append(f"notebook_normalization_check_failed: {e}") + + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw_meta) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + + elif kind == "md": + text = p.read_text(encoding="utf-8") + fm, _body, fm_error = read_md_frontmatter(text) + + if fm_error: + rec.meta = None + rec.warnings.append("invalid_metadata") + rec.errors.append(f"markdown_frontmatter_invalid: {fm_error}") + else: + fm = fm or {} + has_dlc = DLC_NAMESPACE in fm + raw = fm.get(DLC_NAMESPACE) + + if not has_dlc: + rec.meta = None + rec.warnings.append("missing_metadata") + else: + rec.meta, valid = parse_dlc_meta(raw) + if not valid: + rec.meta = None + rec.warnings.append("invalid_metadata") + + else: + rec.meta = None + + except Exception as e: + rec.errors.append(f"metadata_read_failed: {e}") + + # ignore=True means: keep reporting diagnostics, but skip freshness/policy logic + if rec.meta and rec.meta.ignore: + records.append(rec) + continue + + if record_needs_metadata_sync(rec): + rec.warnings.append("metadata_sync_needed") + + last_verified = rec.meta.last_verified if rec.meta else None + rec.days_since_verified = compute_days_since(last_verified, today) + + # Future dates are data errors + if rec.last_content_updated is not None and rec.last_content_updated > today: + rec.errors.append("future_last_content_updated") + if rec.meta and rec.meta.last_metadata_updated is not None: + if rec.meta.last_metadata_updated > today: + rec.errors.append("future_last_metadata_updated") + if last_verified is not None and last_verified > today: + rec.errors.append("future_last_verified") + + pol = cfg.policy + + if ( + rec.days_since_content_update is not None + and rec.days_since_content_update > pol.warn_if_content_older_than_days + ): + rec.warnings.append(f"content_stale>{pol.warn_if_content_older_than_days}d") + + 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") + + records.append(rec) + + return records + + +# ----------------------------- +# Update mode +# ----------------------------- +def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: + """ + Guardrail: writing metadata/normalization without the marker convention will + destroy the meaning of content freshness signals. Require an explicit ack. + """ + if not write: + return + if ack_marker: + return + raise SystemExit( + "Refusing to write without acknowledging metadata-commit convention.\n" + "Re-run with --ack-meta-commit-marker and commit with:\n" + f" {SUGGESTED_TAGGED_COMMIT}\n" + ) + + +def update_files( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, + write: bool, + set_content_date_from_git: bool, + set_last_verified: date | None, + set_verified_for: str | None, + ack_meta_commit_marker: bool, +) -> list[FileRecord]: + today = _iso_today() + records = scan_files(repo_root, cfg, targets=targets) + + for rec in records: + if rec.kind not in {"ipynb", "md"}: + continue + if rec.meta and rec.meta.ignore: + continue + + meta = rec.meta or DLCMeta() + + # Build the desired metadata WITHOUT touching last_metadata_updated. + if set_content_date_from_git and rec.last_content_updated is not None: + meta.last_content_updated = rec.last_content_updated + + if set_last_verified is not None: + meta.last_verified = set_last_verified + if set_verified_for is not None: + meta.verified_for = set_verified_for + + desired_base = meta_to_jsonable(meta) + abs_path = repo_root / rec.path + changed = False + + if rec.kind == "ipynb": + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: + changed = True + if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + nb_meta[DLC_NAMESPACE] = merged_final + write_ipynb_meta(abs_path, nb) + + elif rec.kind == "md": + text = abs_path.read_text(encoding="utf-8") + fm, body, fm_error = read_md_frontmatter(text) + if fm_error: + msg = f"markdown_frontmatter_invalid: {fm_error}" + if msg not in rec.errors: + rec.errors.append(msg) + continue + + fm = fm or {} + + prev = fm.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + + merged_base = dict(prev) + merged_base.update(desired_base) + + if merged_base != prev: + changed = True + if write: + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) + + meta.last_metadata_updated = today + desired_final = meta_to_jsonable(meta) + + merged_final = dict(prev) + merged_final.update(desired_final) + fm[DLC_NAMESPACE] = merged_final + abs_path.write_text(dump_md_frontmatter(fm, body), encoding="utf-8") + + rec.would_change = changed + rec.meta = meta + rec.days_since_verified = compute_days_since(meta.last_verified, today) + + return records + + +# ----------------------------- +# Notebook formatting +# ----------------------------- +def normalize_notebooks( + repo_root: Path, + cfg: ToolConfig, + targets: list[str] | None, + write: bool, + ack_meta_commit_marker: bool, +) -> list[FileRecord]: + """ + Normalize notebooks deterministically (canonical nbformat JSON). + This is intentionally separated from update() because it causes churn. + """ + _require_meta_marker_ack(write=write, ack_marker=ack_meta_commit_marker) + records = scan_files(repo_root, cfg, targets=targets) + today = _iso_today() + + for rec in records: + if rec.kind != "ipynb": + continue + if rec.meta and rec.meta.ignore: + continue + + abs_path = repo_root / rec.path + try: + nb, _raw, _has_dlc = read_ipynb_meta(abs_path) + nbformat.validate(nb) + + if not notebook_is_normalized(abs_path, nb): + rec.would_change = True + if write: + # Update embedded maintenance timestamp + meta = rec.meta or DLCMeta() + meta.last_metadata_updated = today + + nb_meta = nb.setdefault("metadata", {}) + prev = nb_meta.get(DLC_NAMESPACE, {}) + if not isinstance(prev, dict): + prev = {} + merged = dict(prev) + merged.update(meta_to_jsonable(meta)) + nb_meta[DLC_NAMESPACE] = merged + + # Write to persist metadata update (still canonical) + write_ipynb_meta(abs_path, nb) + rec.meta = meta + + except Exception as e: + rec.errors.append(f"normalize_failed: {e}") + + return records + + +# ----------------------------- +# Output formatting +# ----------------------------- + + +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)), + "metadata_sync_needed": sum(1 for r in records if "metadata_sync_needed" in r.warnings), + } + + +def to_markdown(report: Report, cfg: ToolConfig) -> str: + pol = cfg.policy + t = report.totals + lines: list[str] = [] + + lines.append("# 🌡️ DeepLabCut freshness report\n") + lines.append(f"Generated: {report.generated_at.isoformat()}\n") + lines.append(f"Schema: v{report.schema_version}\n\n") + + lines.append("## Summary\n") + lines.append(f"- Files scanned: **{t['files']}**\n") + lines.append(f"- Files with warnings: **{t['warnings']}**\n") + 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") + lines.append(f"- Metadata sync needed: **{t['metadata_sync_needed']}**\n\n") + + 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.sort( + key=lambda r: ( + -(r.days_since_verified or -1), + -(r.days_since_content_update or -1), + r.path, + ) + ) + + if warn_recs: + lines.append("## Warnings\n") + for r in warn_recs: + meta = r.meta + lines.append(f"- **{r.path}** ({r.kind})\n") + lines.append( + f" - last_content_updated: {fmt_date(r.last_content_updated)} " + f"(days: {r.days_since_content_update if r.days_since_content_update is not None else '-'})\n" + ) + 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") + lv = meta.last_verified if meta else None + lines.append( + f" - last_verified: {fmt_date(lv)} " + f"(days: {r.days_since_verified if r.days_since_verified is not None else '-'})\n" + ) + if meta and meta.verified_for: + lines.append(f" - verified_for: {meta.verified_for}\n") + if meta and meta.tier: + lines.append(f" - tier: {meta.tier}\n") + lines.append(f" - warnings: {', '.join(r.warnings)}\n") + if r.errors: + lines.append(f" - errors: {', '.join(r.errors)}\n") + lines.append("\n") + + err_recs = [r for r in report.records if r.errors] + if err_recs: + lines.append("## Scan errors\n") + for r in err_recs: + lines.append(f"- **{r.path}**: {', '.join(r.errors)}\n") + lines.append("\n") + + if report.metadata_sync_targets: + lines.append("## Metadata sync suggestions\n\n") + lines.append( + "The following files have embedded `deeplabcut.last_content_updated` metadata " + "that is missing or out of sync with the git-derived content date:\n\n" + ) + for p in report.metadata_sync_targets: + lines.append(f"- `{p}`\n") + lines.append("\n") + + if report.metadata_sync_command: + lines.append("Run this locally:\n\n") + lines.append("```bash\n") + lines.append(report.metadata_sync_command + "\n") + lines.append("```\n\n") + + git_add = build_git_add_command(report.metadata_sync_targets) + if git_add: + lines.append("Then commit with:\n\n") + lines.append("```bash\n") + lines.append(git_add + "\n") + lines.append(f'git commit -m "{SUGGESTED_TAGGED_COMMIT}"\n') + lines.append("```\n\n") + + lines.append("## Notes\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" + ) + lines.append( + "- In `check` mode, scan/parsing errors are reported for visibility but do not " + "fail by default unless strict mode is enabled or they trigger an enforced policy rule.\n" + ) + return "".join(lines) + + +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") + md_path.write_text(to_markdown(report, cfg), encoding="utf-8") + return json_path, md_path + + +# ----------------------------- +# Check enforcement +# ----------------------------- +def enforce(cfg: ToolConfig, records: list[FileRecord]) -> list[str]: + pol = cfg.policy + violations: list[str] = [] + today = _iso_today() + + for r in records: + if r.meta and r.meta.ignore: + continue + if r.kind not in {"ipynb", "md"}: + continue + + has_invalid_metadata = "invalid_metadata" in (r.warnings or []) + + if match_allowlist(r.path, pol.require_metadata): + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") + elif r.meta is None: + violations.append(f"{r.path}: missing metadata") + + if match_allowlist(r.path, pol.require_recent_verification): + if has_invalid_metadata: + violations.append(f"{r.path}: invalid metadata") + else: + lv = r.meta.last_verified if r.meta else None + if lv is None: + violations.append(f"{r.path}: missing last_verified") + else: + days = (today - lv).days + if days > pol.warn_if_verified_older_than_days: + violations.append( + f"{r.path}: last_verified is {days}d old (> {pol.warn_if_verified_older_than_days}d)" + ) + + if pol.fail_if_metadata_sync_needed and record_needs_metadata_sync(r): + violations.append( + f"{r.path}: embedded last_content_updated is missing or out of sync with git content update date" + ) + + 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)") + + return violations + + +# ----------------------------- +# CLI +# ----------------------------- + + +def parse_date_token(token: str) -> date: + token = token.strip().lower() + if token in {"today", "now"}: + return _iso_today() + return date.fromisoformat(token) + + +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: 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") + + sub = parser.add_subparsers(dest="cmd", required=True) + rep = sub.add_parser("report", help="Generate staleness report (read-only)") + rep.add_argument( + "--targets", + nargs="*", + help=( + "Optional repo-relative targets to limit the operation. " + "Supports exact files, directories, and glob patterns " + "(e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + + chk = sub.add_parser( + "check", + help=( + "Run scans + policy checks (read-only). " + "Fails on enforced policy violations; scan errors are non-fatal by default." + ), + ) + chk.add_argument( + "--targets", + nargs="*", + help=( + "Optional list of relative file paths to scan (limits scan to these files). " + "Supports exact files, directories, and glob patterns (e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + chk.add_argument( + "--strict-mode", + action="store_true", + 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.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) + up.add_argument( + "--set-content-date-from-git", + 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. " + "Supports exact files, directories, and glob patterns (e.g. docs/page.md, docs/gui/, 'docs/**/*.md'). " + "Both '/' and '\\' are accepted." + ), + ) + 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( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) + + norm = sub.add_parser( + "normalize", + help="Normalize notebooks deterministically (write mode requires --write)", + ) + norm.add_argument( + "--write", + action="store_true", + help="Actually write changes (otherwise dry-run)", + ) + norm.add_argument( + "--targets", + nargs="*", + help=( + "Optional list of relative notebook paths to normalize. " + "Supports exact files, directories, and glob patterns " + "(e.g. notebooks/example.ipynb, notebooks/, 'notebooks/**/*.ipynb'). " + "Both '/' and '\\' are accepted." + ), + ) + norm.add_argument( + "--ack-meta-commit-marker", + action="store_true", + help=f"Acknowledge that you will commit changes using marker: {META_COMMIT_MARKER}", + ) + + args = parser.parse_args(list(argv) if argv is not None else None) + + config_path = Path(args.config) + repo_root = find_repo_root(Path.cwd()) + cfg = load_config(config_path) + out_dir = Path(args.out_dir) + + requested_targets = getattr(args, "targets", None) + + if requested_targets: + matched_paths, unmatched_targets = validate_requested_targets(repo_root, cfg, requested_targets) + print_target_match_summary(matched_paths, requested_targets) + + if unmatched_targets: + print("\nUnmatched --targets:") + for raw in unmatched_targets: + print(f"- {raw}") + print( + "\nEach --targets selector must match at least one file in the configured scan set. " + "Use repo-relative paths, directories, or glob patterns " + "(for example: docs/page.md, docs/gui/, 'docs/**/*.md')." + ) + return 2 + + 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 + records = update_files( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + set_content_date_from_git=bool(args.set_content_date_from_git), + set_last_verified=lv, + set_verified_for=args.set_verified_for, + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), + ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") + + else: # normalize + records = normalize_notebooks( + repo_root, + cfg, + targets=args.targets, + write=bool(args.write), + ack_meta_commit_marker=bool(args.ack_meta_commit_marker), + ) + if args.write: + print(f"\nSuggested commit message:\n {SUGGESTED_TAGGED_COMMIT}\n") + + metadata_sync_targets = collect_metadata_sync_targets(records) + metadata_sync_command = build_metadata_sync_command(str(config_path), metadata_sync_targets) + + report = Report( + generated_at=datetime.now(timezone.utc), + repo_root=str(repo_root), + config_path=str(config_path), + totals=summarize(records), + records=records, + metadata_sync_targets=metadata_sync_targets, + metadata_sync_command=metadata_sync_command, + ) + + json_path, md_path = write_outputs(report, cfg, out_dir) + + if metadata_sync_targets and args.cmd in {"report", "check"}: + print(f"\nMetadata sync needed for {len(metadata_sync_targets)} file(s):") + for p in metadata_sync_targets: + print(f"- {p}") + + if metadata_sync_command: + print("\nRun this locally:") + print(metadata_sync_command) + + git_add = build_git_add_command(metadata_sync_targets) + if git_add: + print("\nThen commit with:") + print(git_add) + print(f'git commit -m "{SUGGESTED_TAGGED_COMMIT}"') + + # Emit GitHub Actions job summary if available + emit_summary = not getattr(args, "no_step_summary", False) + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if emit_summary and step_summary and md_path.exists(): + try: + content = md_path.read_text(encoding="utf-8") + # snippet = "\n".join(content.splitlines()[:220]) + "\n" + snippet = "\n".join(content.splitlines()[:]) + "\n" + Path(step_summary).write_text(snippet, encoding="utf-8") + except Exception: + pass + + scan_errors = collect_scan_issues(records, target="errors") + if scan_errors: + print("\nScan errors detected (non-fatal by default):") + for item in scan_errors[:20]: + print(f"- {item}") + if len(scan_errors) > 20: + print(f"... and {len(scan_errors) - 20} more (see report for full details)") + + if args.cmd == "check": + violations = enforce(cfg, records) + if violations: + print("Policy violations:") + for v in violations: + print(f"- {v}") + return 2 + strict_mode = bool((args.strict_mode) or cfg.policy.fail_on_scan_errors) + if strict_mode and scan_errors: + print("Strict mode enabled: failing due to scan/parsing errors.") + return 1 + + # Non-zero if metadata parsing errors occurred for non-report/check commands + if args.cmd not in {"report", "check"} and any(r.errors for r in records): + return 1 + else: + print("\nReport generated:") + print(f"- JSON: {json_path}") + print(f"- Markdown: {md_path}") + + if any(r.warnings for r in records): + print("Warnings detected; see report for details.") + if any(r.errors for r in records): + print("Scan errors detected; see report for details.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/docs_and_notebooks_report_config.yml b/tools/docs_and_notebooks_report_config.yml new file mode 100644 index 0000000000..ee7995b742 --- /dev/null +++ b/tools/docs_and_notebooks_report_config.yml @@ -0,0 +1,28 @@ +version: 1 + +scan: + include: + - "examples/COLAB/**/*.ipynb" + - "examples/JUPYTER/**/*.ipynb" + - "docs/**/*.md" + - "docs/**/*.ipynb" # if notebooks get added to docs (Jupyter Book supports this) + - "tools/**/*.md" + exclude: + - "**/.ipynb_checkpoints/**" + - "**/_build/**" + - "**/build/**" + - "tools/docs_audits/**" + +policy: + warn_if_content_older_than_days: 365 + warn_if_verified_older_than_days: 365 + missing_last_verified_is_warning: true + + # 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, + # or requiring verification for more recent versions. + require_metadata: [] + require_recent_verification: [] + require_notebook_normalized: [] diff --git a/tools/docs_and_notebooks_tool_README.md b/tools/docs_and_notebooks_tool_README.md new file mode 100644 index 0000000000..fa27c5dc8f --- /dev/null +++ b/tools/docs_and_notebooks_tool_README.md @@ -0,0 +1,190 @@ +# Docs & Notebooks Checks Tool + +This tool scans DeepLabCut documentation pages and notebooks and produces **two independent signals**: + +- **`last_content_updated`**: computed from git history as the last *meaningful content* update **excluding metadata-only commits**. +- **`last_verified`**: a human-controlled date indicating the content was verified to work/be accurate. + +In addition, the tool can optionally track: + +- **`last_metadata_updated`**: when the tool last performed a metadata/normalization write (helps explain “file changed” without implying content changed). +- **`verified_for`**: a human-controlled string indicating what the content was verified against (e.g. `3.0.0rc13`). + +The tool is designed to be: + +- **Safe by default**: CI should run **read-only** modes (`report` / `check`). +- **Deterministic**: stable outputs and normalized notebook formatting when explicitly requested. +- **Future-proof**: versioned Pydantic schemas (`schema_version`). + +--- + +## What gets scanned + +Default include patterns are defined in `tools/docs_and_notebooks_report_config.yml`. +Typical patterns include: + +- `examples/COLAB/**/*.ipynb` +- `examples/JUPYTER/**/*.ipynb` +- `docs/**/*.md` +- `docs/**/*.ipynb` (if notebooks are added under docs) + +You can further restrict the scan via `--targets`. + +--- + +## Metadata storage locations + +### Notebooks (`.ipynb`) + +The tool **only** reads/writes **top-level notebook metadata** under the `deeplabcut` namespace. + +> [!IMPORTANT] +> It never edits notebook cells, outputs, or execution counts. + +Example (excerpt): + +```json +{ + "metadata": { + "deeplabcut": { + "last_content_updated": "2020-01-01", + "last_metadata_updated": "2026-03-05", + "last_verified": "2026-02-20", + "verified_for": "3.0.0rc13", + "ignore": false + } + } +} +``` + +> [!NOTE] +> `tier` is intentionally optional and is not auto-populated. + +### Markdown (`.md`) + +The tool reads/writes YAML frontmatter at the top of the file (if present): + +```yaml +--- +deeplabcut: + last_content_updated: 2020-01-01 + last_metadata_updated: 2026-03-05 + last_verified: 2026-02-20 + verified_for: 3.0.0rc13 + ignore: false +--- +``` + +If a doc page has **no** frontmatter, the tool can still report staleness (read-only), and `update` can add/modify metadata when explicitly requested. + +--- + +## The metadata-commit marker (critical) + +Because metadata updates and notebook normalization can rewrite files, they would normally make git (correctly) report that the file was “updated now”. + +To preserve a meaningful **`last_content_updated`**, **all metadata-only / normalization commits must include the marker**: + +- **Marker**: `META_COMMIT_MARKER` (see `tools/docs_and_notebooks_check.py`) +- **Suggested commit message**: `SUGGESTED_META_COMMIT_MESSAGE` + +When you run `update --write` or `normalize --write`, the tool will: + +- Require `--ack-meta-commit-marker` (guardrail) +- Print a suggested commit message + +> [!WARNING] +> If the marker changes in the future, previous iterations still HAVE to be acknowledged to avoid false positives. + +--- + +## Commands + +### 1) Report (read-only) + +Generate a report (does not modify files): + +```bash +python tools/docs_and_notebooks_check.py report +``` + +Writes (by default): + +- `tmp/docs_nb_checks/docs_nb_checks.json` +- `tmp/docs_nb_checks/docs_nb_checks.md` + +### 2) Check (read-only; may fail) + +Run policy checks. By default, CI will not fail unless allowlists are configured. + +```bash +python tools/docs_and_notebooks_check.py check +``` + +The allowlists live in `tools/docs_and_notebooks_report_config.yml`. +They are currently empty, but can help enforce stricter policies once populated (start empty; "ratchet" later). + +### 3) Update metadata (write mode; explicit intent) + +> [!WARNING] +> `update --write` modifies tracked files. Intended for maintainers (manual), not CI. + +#### 3a) Set `last_content_updated` from git (excluding meta commits) + +```bash +python tools/docs_and_notebooks_check.py update --write --set-content-date-from-git --ack-meta-commit-marker +``` + +#### 3b) Set verification fields (human-controlled) + +```bash +python tools/docs_and_notebooks_check.py update --write --targets docs/page.md examples/JUPYTER/foo.ipynb --set-last-verified today --set-verified-for 3.0.0rc13 --ack-meta-commit-marker +``` + +> Tip: omit `--targets` to operate on all scanned files. + +### 4) Normalize notebooks (explicit churn) + +> [!IMPORTANT] +> Notebook normalization rewrites the notebook JSON into a canonical form. +> As such, it is provided as a separate command. + +Dry-run (shows which files *would* change): + +```bash +python tools/docs_and_notebooks_check.py normalize --targets docs/notebook.ipynb +``` + +Write: + +```bash +python tools/docs_and_notebooks_check.py normalize --write --targets docs/notebook.ipynb --ack-meta-commit-marker +``` + +--- + +## CI integration + +Recommended CI usage: + +- Run `report` on PRs and upload the outputs as artifacts. +- Run `check` once allowlists are populated (start empty to avoid failures). + +> [!IMPORTANT] +> Use `actions/checkout` with `fetch-depth: 0` (or sufficiently deep) so `git log` sees history; shallow clones can cause missing or fallback timestamps. + +Dependencies required for this tool (install in the CI job): + +```bash +pip install pydantic pyyaml nbformat +``` + +--- + +## Troubleshooting + +- If you see `content_date_fallback_to_git_touched`, it usually means one of: + - The checkout history is too shallow, or + - *All* commits touching the file are metadata commits with the marker. + +- If Pydantic raises `class-not-fully-defined` errors, ensure the tool calls `.model_rebuild()` for its models (this is already done in the tool). diff --git a/tools/docs_audits/april-2026/audit_metadata.csv b/tools/docs_audits/april-2026/audit_metadata.csv new file mode 100644 index 0000000000..d809f7ec6d --- /dev/null +++ b/tools/docs_audits/april-2026/audit_metadata.csv @@ -0,0 +1,91 @@ +path,kind,metadata_present,visibility,status,recommendation,last_verified,parse_error,validation_error,notes,review_decision,review_priority +docs/Governance.md,md,true,,,,,,,,, +docs/HelperFunctions.md,md,true,online,outdated,archive,,,,"I would suggest using API docs over pages like this to avoid drift. The advice below promises updates that are not being made, and the content is already quite outdated. Automating API docs generation and putting usage info for obtaining commands info in ipython in a basic 'evergreen' page would be more sustainable than trying to maintain this page.",, +docs/MISSION_AND_VALUES.md,md,true,,,,,,,,, +docs/ModelZoo.md,md,true,,,,,,,,, +docs/Overviewof3D.md,md,true,online,review_needed,update,,,,"Contents seem up-to-date as the codebase has not evolved drastically for 3D, but formatting and organization could be improved. Separate basic/advanced sections could help, as well as more admonitions/dropdowns to streamline.",, +docs/README.md,md,true,,,,,,,,, +docs/UseOverviewGuide.md,md,true,,,,,,,,, +docs/beginner-guides/Training-Evaluation.md,md,true,online,viable,move,,,,"As mentioned on other beginner-guides/ docs, this should be part of the GUI section.",, +docs/beginner-guides/beginners-guide.md,md,true,online,outdated,move,,,,Move to GUI section.,, +docs/beginner-guides/labeling.md,md,true,online,viable,move,,,,Move to GUI section. Updated to link directly to the napari plugin docs. Making the link specific to the workflow section of the napari docs could help.,, +docs/beginner-guides/manage-project.md,md,true,online,viable,move,,,,Move to a dedicated GUI section. Making the config edit tool slightly easier to work with and updating the docs below to include additional fields would be helpful.,, +docs/beginner-guides/video-analysis.md,md,true,online,viable,move,,,,"As mentioned on other beginner-guides/ docs, this should be part of the GUI section.",, +docs/benchmark.md,md,true,,,,,,,,, +docs/citation.md,md,true,,,,,,,,, +docs/convert_maDLC.md,md,true,,,,,,,,, +docs/course.md,md,true,online,outdated,archive,,,,,, +docs/dlc-live/deeplabcutlive.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/index.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/quickstart/install.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/aravis_backend.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/gentl_backend.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/opencv_backend.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/misc/misc_landing.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/misc/modelzoo_downloads.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md,md,true,,,,,,,,, +docs/dlc-live/dlc-live-gui/user_guide/overview.md,md,true,,,,,,,,, +docs/dlc-utils/XROMM/usage.md,md,false,,,,,,,,, +docs/dlc-utils/index.md,md,true,,,,2026-05-06,,,,, +docs/docker.md,md,true,online,viable,,2026-05-22,,,,, +docs/gui/PROJECT_GUI.md,md,true,online,review_needed,update,,,,"While the content is generally accurate, repeating installation instructions is not ideal. I would suggest linking to the installation guide instead of re-suggesting commands but then still saying to read the install page... Also, the GUI is likely used by the majority of users, so I would even consider making this a full section in the TOC, and maybe even having one file per GUI tab, which would make tracking code/docs sync easier. Addendum: it seems the beginner guide section is more of a GUI step-by-step, as mentioned earlier in this comment. I would suggest merging/moving and adding links in the present doc, which would make it less of a video list and more of a proper GUI guide.",, +docs/gui/index.md,md,false,,,,,,,,, +docs/gui/napari/advanced_usage.md,md,true,,,,2026-04-09,,,,, +docs/gui/napari/basic_usage.md,md,true,,,,2026-04-09,,,,, +docs/gui/napari/tracking/basic_usage.md,md,true,,,,2026-05-08,,,,, +docs/gui/napari_GUI.md,md,true,online,outdated,archive,2026-04-09,,,Being updated in a separate PR (#3280),, +docs/installation.md,md,true,online,viable,move,2026-04-21,,,Could be moved to a core/installation folder for clarity.,, +docs/intro.md,md,true,,,,,,,,, +docs/maDLC_UserGuide.md,md,true,online,review_needed,verify,,,,"Could use a small formatting pass. Contents are 4-5y old in some places, recommend to review for accuracy.",, +docs/notebooks/extra.md,md,false,,,,,,,,, +docs/notebooks/main_demos.md,md,false,,,,,,,,, +docs/notebooks/your_data.md,md,false,,,,,,,,, +docs/pytorch/Benchmarking_shuffle_guide.md,md,true,online,viable,move,,,,"Useful and well-written, but it could be better grouped with other tutorials/guides rather than being a PyTorch docs only page, as its contents are somewhat in between the two backends.",, +docs/pytorch/architectures.md,md,true,online,viable,keep,,,,,, +docs/pytorch/index.md,md,false,,,,,,,,, +docs/pytorch/pytorch_config.md,md,true,online,review_needed,verify,,,,"Check for accuracy and completeness of content, and update as needed. Formatting is fairly consistent and does not need an urgent update.",, +docs/pytorch/user_guide.md,md,true,,,,,,,,, +docs/pytorch_dlc.md,md,true,orphaned,viable,move,,,,Unclear why this is unlisted in TOC; recommend updating and moving to PyTorch section.,, +docs/quick-start/index.md,md,true,online,viable,keep,2026-05-12,,,,, +docs/quick-start/single_animal_quick_guide.md,md,true,online,viable,archive,,,,"This is a bit stuck between minimal guide and quick start, as the lack of explanations makes it more into a catalogue of commands (which is an API docs responsibility), and a proper quick start guide that gives users a proper sense of the workflow. This should either be expanded greatly or simply archived. For simplicity, I recommend archiving.",, +docs/quick-start/tutorial_maDLC.md,md,true,online,viable,keep,,,,,, +docs/recipes/BatchProcessing.md,md,true,,,,,,,,, +docs/recipes/ClusteringNapari.md,md,true,,,,,,,,, +docs/recipes/DLCMethods.md,md,true,,,,,,,,, +docs/recipes/MegaDetectorDLCLive.md,md,true,orphaned,outdated,archive,,,,,, +docs/recipes/OpenVINO.md,md,true,,,,,,,,, +docs/recipes/OtherData.md,md,true,,,,,,,,, +docs/recipes/TechHardware.md,md,true,online,outdated,update,,,,Useful but needs to be updated and clarified.,, +docs/recipes/UsingModelZooPupil.md,md,true,,,,,,,,, +docs/recipes/external_data_import.md,md,true,,,,2026-05-22,,,,, +docs/recipes/flip_and_rotate.ipynb,ipynb,true,,,,,,,,, +docs/recipes/index.md,md,false,,,,,,,,, +docs/recipes/installTips.md,md,true,online,outdated,archive,,,,Should be removed in favor of the main installation guide.,, +docs/recipes/io.md,md,true,,,,,,,,, +docs/recipes/nn.md,md,true,,,,,,,,, +docs/recipes/pose_cfg_file_breakdown.md,md,true,,,,,,,,, +docs/recipes/post.md,md,true,,,,,,,,, +docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md,md,true,online,review_needed,verify,,,,"Slightly redundant with CONTRIBUTING.md, style may need adjusted based on the rest of the repo.",, +docs/roadmap.md,md,true,,,,,,,,, +docs/standardDeepLabCut_UserGuide.md,md,true,online,review_needed,update,,,,"This is a crucial piece of the doc, but it is rather long and verbose. Recommend breaking it up into smaller sections, and adding more visuals (e.g. screenshots of the GUI, etc.) to make it more engaging and easier to read. Also, consider adding a table of contents at the beginning for easier navigation.",, +examples/COLAB/COLAB_3miceDemo.ipynb,ipynb,true,,,,2026-05-11,,,"This notebook is a demo for the TensorFlow-based pipeline described in the Nature Methods publication (https://doi.org/10.1038/s41592-022-01443-0). However, the corresponding DeepLabCut version does not run anymore on Colab (which requires Python >= 3.11). The notebook has been adapted to use the latest TensorFlow-enabled version of DeepLabCut (namely, 3.0), which supports both PyTorch and TensorFlow.",, +examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb,ipynb,true,,,,,,,,, +examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb,ipynb,true,,,,2026-05-11,,,,, +examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb,ipynb,true,,,,2026-05-11,,,,, +examples/COLAB/COLAB_DLC_ModelZoo.ipynb,ipynb,true,,,,2026-05-11,,,,, +examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb,ipynb,false,,,,,,,,, +examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb,ipynb,true,,,,2026-05-11,,,,, +examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb,ipynb,true,,,,2026-05-11,,,"This notebook demos the primary generic/PyTorch API for single-animal DeepLabCut projects. Note that it is a bit outdated and may need revisions after dropping TensorFlow support. Also it does not reflect any of the planned refactors currently in the works (e.g. structured configs, keypoints).",, +examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb,ipynb,true,,,,2026-05-11,,,"This notebook demos the primary generic/PyTorch API for multi-animal DeepLabCut (maDLC) projects. Note that it may need revisions after dropping TensorFlow support. And does not reflect any of the planned refactors currently in the works (e.g. structured configs, keypoints).",, +examples/COLAB/COLAB_transformer_reID.ipynb,ipynb,false,,,,,,,,, +examples/JUPYTER/Demo_3D_DeepLabCut.ipynb,ipynb,true,,,,,,,,, +examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb,ipynb,true,,,,,,,,, +examples/JUPYTER/Demo_labeledexample_Openfield.ipynb,ipynb,true,,,,,,,,, +examples/JUPYTER/Demo_napari.ipynb,ipynb,true,,,,,,,,, +examples/JUPYTER/Demo_yourowndata.ipynb,ipynb,true,,,,,,,,, +examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb,ipynb,true,,,,,,,,, +tools/README.md,md,false,,,,,,,,, +tools/docs_and_notebooks_tool_README.md,md,false,,,,,,,,, +tools/ruff_cleanup_helpers.md,md,false,,,,,,,,, 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() diff --git a/tools/ruff_cleanup_helpers.md b/tools/ruff_cleanup_helpers.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/ruff_report.py b/tools/ruff_report.py new file mode 100644 index 0000000000..bfa7e6f206 --- /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 ruff_report.py . --output ruff-report.md + python 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 = [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) + 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="tmp/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()) diff --git a/tools/test_selector.py b/tools/test_selector.py new file mode 100644 index 0000000000..5083d77d6a --- /dev/null +++ b/tools/test_selector.py @@ -0,0 +1,1032 @@ +#!/usr/bin/env python3 +"""Deterministic, strictly validated test selector for DeepLabCut. + +Outputs orthogonal workflow mode selections plus structured test selections: + + - lanes: which workflow lanes should run (skip, docs, fast, full) + - pytest_paths: JSON list of pytest path arguments + - functional_scripts: JSON list of python script paths + - provenance: JSON mapping each selected test/script to the category rules that selected it + +Safety principles +----------------- +- Fail-safe: if changes cannot be determined or are ambiguous, the "full" lane is always selected. +- Deterministic: derives diff range from GitHub Actions event payload when available. + * pull_request: uses merge-base(base.sha, head.sha) .. head.sha + * push: uses before .. after + * manual override: uses exactly --base-sha .. --head-sha + * fallback: attempts HEAD~1 .. HEAD +- Secure: never emits shell command strings; only structured data. +- Strict: Pydantic schema validation (extra=forbid), SHA validation, path sanitization. + +Intended usage in GitHub Actions +------------------------------- +- Checkout with sufficient history for merge-base/diff (typically fetch-depth: 0). +- Run: + python tools/test_selector.py --write-github-output --json + +This will write the following keys to $GITHUB_OUTPUT: + - run_skip (bool): whether to run the skip mode + - run_docs (bool): whether to run the docs workflow + - run_fast (bool): whether to run targeted test execution + - run_full (bool): whether to run the full matrix/full suite workflow + - selected_workflows (list): list of selected workflow lanes + - lane_reasons (dict): reasons for selecting each workflow lane + - diff_mode (str): how the diff was determined + - pytest_paths (list): list of pytest path arguments + - functional_scripts (list): list of python script paths + - reasons (list): aggregate machine-readable reasons for the selection + - changed_files (list): list of changed files + - provenance (dict): mapping each selected test/script to the category rules that selected it + +Notes +----- +- This script intentionally keeps the routing rules simple and location-based. +- Extend CATEGORY_RULES and FULL_SUITE_TRIGGERS as needed, keeping rules auditable. +""" + +from __future__ import annotations + +import argparse +import json +import os +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 + +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +try: + from .test_selector_config import ( + CATEGORY_RULE_BY_NAME, + CATEGORY_RULES, + FULL_SUITE_TRIGGERS, + FUNC_SCRIPT_DEPENDENCIES, + LINT_ONLY_FILES, + MINIMAL_PYTEST, + ) +# Allows to run as "python tools/test_selector.py" without installing as a package, +# but still import the config from the same location. +except ImportError: # pragma: no cover + from test_selector_config import ( + CATEGORY_RULE_BY_NAME, + CATEGORY_RULES, + FULL_SUITE_TRIGGERS, + FUNC_SCRIPT_DEPENDENCIES, + LINT_ONLY_FILES, + MINIMAL_PYTEST, + ) + + +SHA_RE = re.compile(r"^[0-9a-f]{7,40}$", re.IGNORECASE) +# DLC_NAMESPACE = "deeplabcut" + + +class DiffMode(str, Enum): + """How the diff was determined, for auditing and reporting.""" + + PR = "pr" # merge-base(base, head) .. head + PUSH = "push" # before .. after + MANUAL = "manual" # explicit base...head from CLI args + FALLBACK = "fallback" # HEAD^ .. HEAD + INITIAL = "initial" # empty tree .. HEAD + FALLBACK_NO_HEAD = "fallback_no_head" # couldn't resolve HEAD + + +MODE_LABELS = { + DiffMode.PR: "Pull request (merge-base..HEAD)", + DiffMode.PUSH: "Push (before..after)", + DiffMode.MANUAL: "Manual override", + DiffMode.FALLBACK: "Fallback (HEAD^..HEAD)", + DiffMode.INITIAL: "Initial commit (empty tree..HEAD)", + DiffMode.FALLBACK_NO_HEAD: "Fallback (couldn't resolve HEAD)", +} + + +class LaneSelection(BaseModel): + """Which workflow lanes should run.""" + + model_config = ConfigDict(extra="forbid") + + 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 in test workflow + full: bool = False # Delegate to full test workflow/matrix + + +class SelectionProvenance(BaseModel): + """Why each selected test/script path was included.""" + + model_config = ConfigDict(extra="forbid") + + pytest: dict[str, list[str]] = Field(default_factory=dict) + scripts: dict[str, list[str]] = Field(default_factory=dict) + + +class SelectorResult(BaseModel): + """Strict output schema.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: int = 2 + diff_mode: DiffMode = DiffMode.FALLBACK_NO_HEAD + + lanes: LaneSelection = Field(default_factory=LaneSelection) + + 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) + + +SelectorResult.model_rebuild() # Ensure model is fully built at import time for validation in main() + + +# ----------------------------- +# Git helpers +# ----------------------------- +def _run_git(args: Sequence[str], cwd: Path) -> str: + proc = subprocess.run( + ["git", *args], + cwd=str(cwd), + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(f"git {' '.join(args)} failed: {proc.stderr.strip()}") + return proc.stdout.strip() + + +def find_repo_root() -> Path: + out = _run_git(["rev-parse", "--show-toplevel"], Path.cwd()) + return Path(out).resolve() + + +def _validate_sha(label: str, sha: str) -> str: + if not sha or not SHA_RE.match(sha): + raise ValueError(f"Invalid {label} SHA: {sha!r}") + return sha + + +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]: + path = os.environ.get("GITHUB_EVENT_PATH") + if not path: + return {} + try: + return json.loads(Path(path).read_text(encoding="utf-8")) + except Exception: + return {} + + +def _normalize_relpath(p: str) -> str: + """Normalize and validate a repo-relative path from git output.""" + if "\x00" in p: + raise ValueError("NUL byte in path") + p = p.strip().replace("\\", "/") + if not p: + raise ValueError("Empty path") + if p.startswith("/") or re.match(r"^[A-Za-z]:/", p): + raise ValueError(f"Absolute path not allowed: {p}") + parts = [x for x in p.split("/") if x not in ("", ".")] + if any(x == ".." for x in parts): + raise ValueError(f"Path traversal not allowed: {p}") + return "/".join(parts) + + +def _empty_tree(repo: Path) -> str: + # Avoid hardcoding; derive the empty tree hash deterministically. + empty = _run_git(["hash-object", "-t", "tree", os.devnull], repo) + return _validate_sha("empty-tree", empty) + + +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", "") + event = _load_github_event() + + if override_base and override_head: + base = _validate_sha("base", override_base) + head = _validate_sha("head", override_head) + _ensure_commit_exists(base, repo) + _ensure_commit_exists(head, repo) + return base, head, DiffMode.MANUAL + + if event_name == "pull_request" and "pull_request" in event: + base_sha = _validate_sha("base", event["pull_request"]["base"]["sha"]) + head_sha = _validate_sha("head", event["pull_request"]["head"]["sha"]) + _ensure_commit_exists(base_sha, repo) + _ensure_commit_exists(head_sha, repo) + # Use merge-base to approximate the PR triple-dot diff base deterministically. + merge_base = _run_git(["merge-base", head_sha, base_sha], repo) + merge_base = _validate_sha("merge-base", merge_base) + _ensure_commit_exists(merge_base, repo) + return merge_base, head_sha, DiffMode.PR + + if event_name == "push" and "before" in event and "after" in event: + before = _validate_sha("before", event["before"]) + after = _validate_sha("after", event["after"]) + _ensure_commit_exists(after, repo) + + if before == zero_sha: + empty = _empty_tree(repo) + return empty, after, DiffMode.INITIAL + try: + _ensure_commit_exists(before, repo) + return before, after, DiffMode.PUSH + except Exception: + empty = _empty_tree(repo) + return empty, after, DiffMode.INITIAL + + # Fallback: try parent..HEAD; if no parent (initial commit), diff empty-tree..HEAD + try: + head = _validate_sha("HEAD", _run_git(["rev-parse", "HEAD"], repo)) + _ensure_commit_exists(head, repo) + + try: + prev = _validate_sha("HEAD^", _run_git(["rev-parse", "--verify", "HEAD^"], repo)) + _ensure_commit_exists(prev, repo) + return prev, head, DiffMode.FALLBACK + except Exception: + # Initial commit (no parent): treat as "everything added" + empty = _empty_tree(repo) + return empty, head, DiffMode.INITIAL + except Exception: + return "", "", DiffMode.FALLBACK_NO_HEAD + + +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) + files = [_normalize_relpath(line) for line in out.splitlines() if line.strip()] + return sorted(set(files)) + + +def _is_safe_relpath(p: str) -> bool: + """Safety check for a git-relative path: no absolute, no traversal, no NUL.""" + return ( + p + and "\x00" not in p + and not p.startswith("/") + and not re.match(r"^[A-Za-z]:/", p) + and ".." not in Path(p).parts + ) + + +def validate_selected_paths(res: SelectorResult, repo: Path) -> SelectorResult: + missing: list[str] = [] + + # validate pytest paths (files/dirs) + for p in res.pytest_paths: + if not _is_safe_relpath(p) or not (repo / p).exists(): + missing.append(f"pytest:{p}") + + # validate functional scripts (files) + for s in res.functional_scripts: + if not _is_safe_relpath(s) or not (repo / s).exists(): + missing.append(f"script:{s}") + + if missing: + # Fail-safe escalation: disable fast lane selection, enable full lane. + # Preserve docs lane if it was independently selected. + res.lanes.fast = False + res.lanes.full = True + res.pytest_paths = [] + res.functional_scripts = [] + res.provenance = SelectionProvenance() + res.reasons = res.reasons + ["missing_selected_paths"] + missing + + lane_reasons = dict(res.lane_reasons) + lane_reasons.pop("fast", None) + full_reasons = list(lane_reasons.get("full", [])) + full_reasons.extend(["missing_selected_paths", *missing]) + lane_reasons["full"] = full_reasons + res.lane_reasons = lane_reasons + + return res + + +# ----------------------------- +# Decision logic +# ----------------------------- + + +def _matches_any(path: str, preds: Sequence[Callable[[str], bool]]) -> bool: + for pred in preds: + try: + if pred(path): + return True + except Exception: + continue + return False + + +def resolve_functional_scripts( + selected: set[str], +) -> tuple[list[str], dict[str, set[str]]]: + """Expand and dependency-order functional scripts. + + Returns: + ordered + Selected scripts and their transitive dependencies, with each + dependency appearing before scripts that depend on it. + dependency_sources + Provenance entries for every dependency added during expansion. + """ + ordered: list[str] = [] + dependency_sources: dict[str, set[str]] = defaultdict(set) + visited: set[str] = set() + visiting: set[str] = set() + + def visit(script: str) -> None: + if script in visited: + return + + if script in visiting: + raise ValueError(f"Cyclic functional-script dependency involving {script!r}") + + visiting.add(script) + + for dependency in FUNC_SCRIPT_DEPENDENCIES.get( + script, + (), + ): + dependency_sources[dependency].add(f"dependency:{script}") + visit(dependency) + + visiting.remove(script) + visited.add(script) + ordered.append(script) + + for script in sorted(selected): + visit(script) + + return ordered, dependency_sources + + +def order_functional_scripts( + selected: set[str], +) -> list[str]: + """Expand and order functional scripts after their dependencies.""" + ordered: list[str] = [] + visited: set[str] = set() + visiting: set[str] = set() + + def visit(script: str) -> None: + if script in visited: + return + + if script in visiting: + raise ValueError(f"Cyclic functional-script dependency involving {script!r}") + + visiting.add(script) + + for dependency in FUNC_SCRIPT_DEPENDENCIES.get( + script, + (), + ): + visit(dependency) + + visiting.remove(script) + visited.add(script) + ordered.append(script) + + for script in sorted(selected): + visit(script) + + return ordered + + +def decide(files: list[str]) -> SelectorResult: + reasons: list[str] = [] + lane_reasons: dict[str, list[str]] = {} + lanes = LaneSelection() + + if not files: + lanes.full = True + full_reasons = ["no_changed_files_or_diff_unavailable"] + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=full_reasons, + changed_files=[], + lane_reasons={"full": full_reasons}, + ) + + # Lint-only filtering (routing should ignore these files) + lint_only = [f for f in files if f in LINT_ONLY_FILES] + routed_files = [f for f in files if f not in LINT_ONLY_FILES] + + if lint_only: + reasons.append(f"lint_only_count:{len(lint_only)}") + + # If *only* lint-only files changed, skip all lanes + if not routed_files: + lanes.skip = True + skip_reasons = [*reasons, "lint_only"] + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=skip_reasons, + changed_files=files, + lane_reasons={"skip": skip_reasons}, + ) + + # 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)} + 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] = [] + + 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 [] + + # Full-suite triggers always win over fast, but docs lane can still remain enabled. + triggered: list[tuple[str, str]] = [] + for f in routed_files: + for name, pred in FULL_SUITE_TRIGGERS: + if _matches_any(f, [pred]): + triggered.append((f, name)) + + if triggered: + lanes.full = True + full_reasons = [ + "full_suite_trigger", + f"full_suite_trigger_count:{len(triggered)}", + ] + reasons.extend(full_reasons) + lane_reasons["full"] = full_reasons + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Match NON-doc categories only for test-routing / escalation logic. + matched_non_docs = [] + for rule in CATEGORY_RULES: + if rule.name == "docs": + continue + if any(_matches_any(f, rule.match_any) for f in routed_files): + matched_non_docs.append(rule) + + matched_non_docs = sorted(matched_non_docs, key=lambda r: r.name) + + 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) + + # Docs rules may contribute tests/scripts to the fast lane. + if docs_touched: + for p in docs_pytests_sorted: + pytest_paths_set.add(p) + pytest_sources[p].add("docs") + for s in docs_scripts_sorted: + functional_set.add(s) + script_sources[s].add("docs") + + # Non-doc matched categories contribute to fast lane. + for rule in matched_non_docs: + cat = rule.name + for p in rule.pytest_paths or []: + pytest_paths_set.add(p) + pytest_sources[p].add(cat) + for s in rule.functional_scripts or []: + functional_set.add(s) + script_sources[s].add(cat) + + # If we matched non-doc categories but none provided explicit tests/scripts, + # fall back to the minimal pytest lane. + fallback_used = False + if not pytest_paths_set and not functional_set and matched_non_docs: + for p in MINIMAL_PYTEST: + pytest_paths_set.add(p) + pytest_sources[p].add("fallback_minimal_pytest") + reasons.append("fallback_minimal_pytest") + fallback_used = True + + # If the routed changes are truly docs-only (no non-doc files remain) and no + # tests were selected, return docs lane only. + if not pytest_paths_set and not functional_set: + if lanes.docs and not non_docs_routed_files: + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Otherwise fail-safe to full when nothing matched at all. + lanes.full = True + full_reasons = ["no_category_matched"] + reasons.extend(full_reasons) + lane_reasons["full"] = full_reasons + return SelectorResult( + lanes=lanes, + pytest_paths=[], + functional_scripts=[], + provenance=SelectionProvenance(), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + # Fast lane selected + lanes.fast = True + + 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) + if fallback_used: + fast_reasons.append("fallback_minimal_pytest") + lane_reasons["fast"] = fast_reasons + + ordered_functional_scripts, dependency_sources = resolve_functional_scripts(functional_set) + for script, srcs in dependency_sources.items(): + script_sources[script].update(srcs) + + return SelectorResult( + lanes=lanes, + pytest_paths=sorted(pytest_paths_set), + functional_scripts=ordered_functional_scripts, + provenance=SelectionProvenance( + pytest={path: sorted(sources) for path, sources in sorted(pytest_sources.items())}, + scripts={path: sorted(sources) for path, sources in sorted(script_sources.items())}, + ), + reasons=reasons, + changed_files=files, + lane_reasons=lane_reasons, + ) + + +# ----------------------------- +# Outputs +# ----------------------------- +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] = [] + + # Prep category predicates + categories = [(r.name, r.match_any) for r in CATEGORY_RULES] + + for f in files: + # Which full-suite triggers does this file match? + ft = [] + for trig_name, pred in FULL_SUITE_TRIGGERS: + try: + if pred(f): + ft.append(trig_name) + except Exception: + continue + + # Which categories does it match? + cats = [] + for cat_name, preds in categories: + if _matches_any(f, preds): + cats.append(cat_name) + is_lint_only = f in LINT_ONLY_FILES + + per_file[f] = { + "full_triggers": ft, + "categories": cats, + "lint_only": is_lint_only, + } + + if ft: + for t in ft: + full_trigger_files[t].append(f) + + if cats: + for c in cats: + by_category[c].append(f) + + if is_lint_only: + lint_only_files.append(f) + else: + # Only uncategorized if it matched no categories AND no full-suite triggers + if not ft and not cats: + uncategorized.append(f) + + # Deterministic ordering + for t in full_trigger_files: + full_trigger_files[t] = sorted(set(full_trigger_files[t])) + for c in by_category: + by_category[c] = sorted(set(by_category[c])) + + return { + "per_file": per_file, + "full_trigger_files": dict(full_trigger_files), + "by_category": dict(by_category), + "lint_only": sorted(set(lint_only_files)), + "uncategorized": sorted(set(uncategorized)), + } + + +def _render_file_line( + f: str, + info: dict[str, Any], + emoji: bool = False, + add_tag: bool = True, + add_marker: bool = False, + category_only: bool = True, +) -> str: + # Optional, single marker only + marker = "" + if add_marker: + if info.get("full_triggers"): + marker = "⚠️ " if emoji else "" + elif info.get("lint_only"): + marker = "🧹 " if emoji else "" + elif not info.get("categories"): + marker = "❓ " if emoji else "" + + tags = [] + if add_tag: + if info.get("categories"): + header = "🏷️ " if emoji else "Category match :" + tags.append(f"{header} " + ", ".join(info["categories"])) + if info.get("full_triggers") and not category_only: + header = "🚨 " if emoji else "Full triggers" + tags.append(f"{header} " + ", ".join(info["full_triggers"])) + if info.get("lint_only") and not category_only: + header = "🧹 " if emoji else "Lint-only :" + tags.append(f"{header}") + + tag_str = (" — " + " | ".join(tags)) if tags else "" + return f"- {marker}`{f}`{tag_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)] + + +def _lane_label(name: str, emoji: bool = False) -> str: + if not emoji: + return name + return { + "skip": "⏩ skip", + "docs": "📚 docs", + "fast": "⚡ fast", + "full": "🧪 full", + }.get(name, name) + + +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 = [] + if cats: + out.append("categories: " + ", ".join(cats)) + out.extend(other) + return out + + +def _details_open(summary: str, add_blank: bool = True) -> str: + s = f"
{summary}\n" + if add_blank: + s += "\n" + return s + + +def _details_close() -> str: + return "\n
\n" + + +def _render_decision_markdown( + res: SelectorResult, + limit: int = 40, + style: str = "minimal", + emoji: bool = False, +) -> str: + def bullet(items: list[str], limit_: int = limit) -> str: + if not items: + return "_(none)_" + shown = items[:limit_] + s = "\n".join(f"- `{x}`" for x in shown) + if len(items) > limit_: + s += f"\n- … and {len(items) - limit_} more" + return s + + # 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) + else: + selected_lanes_label = ", ".join(f"`{name}`" for name in selected_lanes) + + if not selected_lanes_label: + selected_lanes_label = "_(none)_" + + diff_mode = f"{MODE_LABELS.get(res.diff_mode, res.diff_mode.value)}" + + 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") + + # Reasons (compacted) + md.append("## Why\n") + for r in _compact_reasons(res.reasons): + md.append(f"- `{r}`") + md.append("") + + if style == "detailed" and res.lane_reasons: + md.append("## Workflow lanes\n") + for lane in _enabled_lane_names(res): + lane_rs = res.lane_reasons.get(lane, []) + md.append(f"### `{lane}`") + if lane_rs: + for r in lane_rs: + md.append(f"- `{r}`") + else: + md.append("_(none)_") + md.append("") + + # Explain changed files + exp = explain_changed_files(res.changed_files) + + md.append("## Changed files (explained)\n") + + # 1) Collapsible: Files that match full-suite triggers + # (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})")) + 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)})") + for f in files_for_trigger[:limit]: + md.append(_render_file_line(f, exp["per_file"][f], emoji=emoji)) + if len(files_for_trigger) > limit: + md.append(f"- … and {len(files_for_trigger) - limit} more") + md.append("") + md.append(_details_close()) + + # 2) Files grouped by category (includes uncategorized and lint-only as collapsible lists) + md.append("### Files grouped by category\n") + + if exp["by_category"]: + for cat in sorted(exp["by_category"].keys()): + files = exp["by_category"][cat] + + # Determine if this category has any explicit selection rules attached + rule = CATEGORY_RULE_BY_NAME.get(cat) + has_rules = bool(rule and (rule.pytest_paths or rule.functional_scripts)) + note = "" if has_rules else " — no specific testing rules attached" + + md.append(_details_open(f"{cat} ({len(files)}){note}")) + for f in files[:limit]: + # Already grouped by category; keep lines clean + md.append(f"- `{f}`") + if len(files) > limit: + md.append(f"- … and {len(files) - limit} more") + md.append(_details_close()) + else: + md.append("_(none)_\n") + + # 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("") + for f in lint_files[:limit]: + md.append(f"- `{f}`") + if len(lint_files) > limit: + md.append(f"- … and {len(lint_files) - limit} more") + md.append(_details_close()) + + # Uncategorized as collapsible — and clarify what it means + # IMPORTANT: explain_changed_files() already ensures that files that match ANY category + # never land here. This section is only for truly unmatched files. + if exp["uncategorized"]: + unc_files = exp["uncategorized"] + md.append( + _details_open( + f"Uncategorized ({len(unc_files)}) — no matching category (no specific testing rules attached)" + ) + ) + md.append("") + for f in unc_files[:limit]: + md.append(_render_file_line(f, exp["per_file"][f], emoji=emoji)) + if len(unc_files) > limit: + md.append(f"- … and {len(unc_files) - limit} more") + md.append(_details_close()) + + if style == "detailed": + md.append("## Changed files (raw)\n") + md.append(bullet(res.changed_files)) + md.append("") + + # Selected tests + md.append("## Selected tests\n") + md.append(_details_open("Pytest paths")) + md.append(bullet(res.pytest_paths)) + md.append(_details_close()) + md.append(_details_open("Functional scripts")) + md.append(bullet(res.functional_scripts)) + md.append(_details_close()) + + # Provenance collapsed by default, only if detailed + if style == "detailed": + md.append("## Provenance\n") + md.append(_details_open("Why these tests")) + md.append("") + + if res.provenance.pytest: + md.append("### Pytest\n") + for p, srcs in res.provenance.pytest.items(): + md.append(f"- `{p}` ← {', '.join(f'`{s}`' for s in srcs)}") + else: + md.append("### Pytest\n_(none)_") + + if res.provenance.scripts: + md.append("\n### Scripts\n") + for s, srcs in res.provenance.scripts.items(): + md.append(f"- `{s}` ← {', '.join(f'`{x}`' for x in srcs)}") + else: + md.append("\n### Scripts\n_(none)_") + + md.append(_details_close()) + + return "\n".join(md) + + +def write_report_files( + res: SelectorResult, + out_dir: Path, + report_style: str = "minimal", + no_emoji: bool = False, +) -> tuple[Path, Path]: + out_dir.mkdir(parents=True, exist_ok=True) + json_path = out_dir / "selection.json" + md_path = out_dir / "decision.md" + + json_path.write_text(res.model_dump_json(indent=2), encoding="utf-8") + md_path.write_text( + _render_decision_markdown(res, style=report_style, emoji=not no_emoji), + encoding="utf-8", + ) + return json_path, md_path + + +def create_job_summary(md_path: Path, overwrite: bool = True) -> None: + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + # Append markdown to the GitHub Actions Job Summary + mode = "w" if overwrite else "a" + with open(summary_path, mode, encoding="utf-8") as f: + f.write(md_path.read_text(encoding="utf-8")) + f.write("\n") + + +def write_github_output(res: SelectorResult) -> None: + out_path = os.environ.get("GITHUB_OUTPUT") + if not out_path: + raise RuntimeError("GITHUB_OUTPUT is not set") + + def j(v) -> str: + return json.dumps(v, separators=(",", ":"), ensure_ascii=False) + + selected_workflows = _enabled_lane_names(res) + + with open(out_path, "a", encoding="utf-8") as f: + f.write(f"run_skip={str(res.lanes.skip).lower()}\n") + f.write(f"run_docs={str(res.lanes.docs).lower()}\n") + f.write(f"run_fast={str(res.lanes.fast).lower()}\n") + f.write(f"run_full={str(res.lanes.full).lower()}\n") + f.write(f"selected_workflows={j(selected_workflows)}\n") + f.write(f"lane_reasons={j(res.lane_reasons)}\n") + f.write(f"diff_mode={res.diff_mode.value}\n") + f.write(f"pytest_paths={j(res.pytest_paths)}\n") + f.write(f"functional_scripts={j(res.functional_scripts)}\n") + f.write(f"reasons={j(res.reasons)}\n") + f.write(f"changed_files={j(res.changed_files)}\n") + f.write(f"provenance={j(res.provenance.model_dump())}\n") + + +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( + "--write-github-output", + action="store_true", + help="Write outputs to $GITHUB_OUTPUT", + ) + ap.add_argument( + "--base-sha", + default=None, + help="Override base commit SHA for manual diff selection (must be used with --head-sha)", + ) + ap.add_argument( + "--head-sha", + default=None, + help="Override head commit SHA for manual diff selection (must be used with --base-sha)", + ) + + ap.add_argument( + "--report-dir", + default="tmp/test-selection", + help="Directory to write decision report files (selection.json, decision.md)", + ) + ap.add_argument( + "--write-summary", + action="store_true", + help="Append decision.md to GitHub Actions Job Summary if available", + ) + ap.add_argument( + "--report-style", + choices=["minimal", "detailed"], + default="detailed", + help="Decision markdown verbosity: minimal or detailed (default: detailed)", + ) + ap.add_argument( + "--no-emoji", + action="store_true", + help="Disable emojis in markdown report (default: off)", + ) + + args = ap.parse_args(list(argv) if argv is not None else None) + if bool(args.base_sha) != bool(args.head_sha): + ap.error("Both --base-sha and --head-sha must be provided together") + + repo = find_repo_root() + + base, head, diff_mode = determine_diff_range(repo, args.base_sha, args.head_sha) + files = changed_files(repo, base, head) + + res = decide(files) + res.diff_mode = diff_mode + res.changed_files = files + res = validate_selected_paths(res, repo) + + # Strict validation + try: + res = SelectorResult.model_validate(res.model_dump()) + except ValidationError as e: + raise RuntimeError(f"Output validation failed: {e}") from e + + # 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) + + if args.json: + print(res.model_dump_json(indent=2)) + + if args.write_github_output: + write_github_output(res) + + # Write Job Summary (GitHub renders markdown from $GITHUB_STEP_SUMMARY) + if args.write_summary: + create_job_summary(md_path) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test_selector_config.py b/tools/test_selector_config.py new file mode 100644 index 0000000000..d9116cb122 --- /dev/null +++ b/tools/test_selector_config.py @@ -0,0 +1,345 @@ +"""Test selector configuration.""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from pathlib import PurePosixPath + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +PathPred = Callable[[str], bool] + +FUNC_SCRIPT_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "examples/testscript_3d.py": ("examples/testscript_tensorflow_single_animal.py",), +} + + +def prefix(*values: str) -> PathPred: + """Match if path starts with any of the given prefixes.""" + prefixes = tuple(values) + return lambda p: p.startswith(prefixes) + + +def suffix(*values: str) -> PathPred: + """Match if path ends with any of the given suffixes.""" + suffixes = tuple(values) + return lambda p: p.endswith(suffixes) + + +def equals(*values: str) -> PathPred: + """Match if path equals any of the given exact values.""" + allowed = frozenset(values) + return lambda p: p in allowed + + +def case_insensitive_match(*values: str) -> PathPred: + """Case-insensitive substring match against any of the given values.""" + needles = tuple(v.lower() for v in values) + return lambda p: any(n in p.lower() for n in needles) + + +def all_of(*preds: PathPred) -> PathPred: + """Logical AND over predicates.""" + return lambda p: all(pred(p) for pred in preds) + + +# ----------------------------- +# Rules validation models +# ----------------------------- +_RULE_NAME_RE = re.compile(r"^[a-z0-9_]+$") + + +def _validate_relpath_string(value: str, field_name: str) -> str: + """Validate a repo-relative path string used in config.""" + if not isinstance(value, str): + raise TypeError(f"{field_name} entries must be strings") + + value = value.strip() + if not value: + raise ValueError(f"{field_name} entries must not be empty") + + value = value.replace("\\", "/") + + if value.startswith("/"): + 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}") + + parts = PurePosixPath(value).parts + if ".." in parts: + raise ValueError(f"{field_name} must not contain path traversal: {value!r}") + + if "\x00" in value: + raise ValueError(f"{field_name} must not contain NUL bytes: {value!r}") + + return value + + +class CategoryRule(BaseModel): + """Validated test-selection category rule.""" + + model_config = ConfigDict(extra="forbid") + + 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."), + ) + pytest_paths: list[str] = Field( + default_factory=list, + description="Pytest paths selected if the rule is triggered.", + ) + functional_scripts: list[str] = Field( + default_factory=list, + description="Functional test scripts selected if the rule is triggered.", + ) + + @field_validator("name") + @classmethod + def validate_name(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("Rule name must not be empty") + if not _RULE_NAME_RE.match(value): + raise ValueError(f"Rule name must match ^[a-z0-9_]+$ (got {value!r})") + return value + + @field_validator("match_any") + @classmethod + def validate_match_any(cls, preds: list[PathPred]) -> list[PathPred]: + if not preds: + 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__}") + return preds + + @field_validator("pytest_paths") + @classmethod + def validate_pytest_paths(cls, values: list[str]) -> list[str]: + return [_validate_relpath_string(v, "pytest_paths") for v in values] + + @field_validator("functional_scripts") + @classmethod + def validate_functional_scripts(cls, values: list[str]) -> list[str]: + return [_validate_relpath_string(v, "functional_scripts") for v in values] + + +def validate_category_rules(rules: list[CategoryRule]) -> list[CategoryRule]: + """Validate cross-rule invariants.""" + seen: dict[str, int] = {} + + 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} at indexes {first_idx} and {idx}") + seen[rule.name] = idx + + return rules + + +# ----------------------------- +# Configuration +# ----------------------------- +MINIMAL_PYTEST = ["tests/test_auxiliaryfunctions.py"] + +POSE_TF = "deeplabcut/pose_estimation_tensorflow/" +POSE_PT = "deeplabcut/pose_estimation_pytorch/" + + +# Conservative full-suite triggers: if any changed file matches, plan=FULL. +FULL_SUITE_TRIGGERS = [ + ("Tests files changed", prefix("tests/")), + ("pyproject.toml changed", equals("pyproject.toml")), + ("lockfile changed", suffix(".lock")), + ("DEEPLABCUT.yaml changed", suffix("DEEPLABCUT.yaml")), + ("CI workflow changed", prefix(".github/workflows/")), +] + + +# Files that should be enforced by dedicated lint workflows, not by test selection +LINT_ONLY_FILES = { + ".pre-commit-config.yaml", + # ".pre-commit-hooks.yaml", +} + + +# The per-file/folder rules that determine test selection logic. Each rule has: +# - a name (for auditing/debugging purposes) +# - a set of path predicates (match_any) that trigger the rule if any predicate +# matches any changed file +# - a list of pytest paths to select if the rule is triggered (can be empty) +# - a list of functional test scripts to select if the rule is triggered (can be empty) +CATEGORY_RULES = validate_category_rules( + [ + # DOCS & NOTEBOOKS # + CategoryRule( + name="docs", + match_any=[ + prefix("docs/"), + all_of(suffix(".md", ".rst"), case_insensitive_match("docs")), + all_of(suffix(".ipynb"), case_insensitive_match("docs")), + equals("_config.yml", "_toc.yml"), + equals(".github/workflows/build-book.yml"), + ], + pytest_paths=[ + # NOTE: + # Optional docs-targeted tests may be attached here. + # If present, docs changes will still enable the docs lane, and may also + # contribute selections added here to the fast lane. + ], + functional_scripts=[ + # NOTE: + # Optional docs-targeted functional tests may be attached here. + # If present, docs changes will still enable the docs lane, and may also + # contribute selections added here to the fast lane. + ], + ), + CategoryRule( + name="notebooks_examples", + match_any=[ + prefix("examples/JUPYTER/", "examples/COLAB/"), + all_of(suffix(".ipynb"), case_insensitive_match("examples")), + ], + pytest_paths=MINIMAL_PYTEST, + functional_scripts=[], + ), + # CORE FUNCTIONALITY # + CategoryRule( + name="superanimal_modelzoo", + match_any=[ + prefix("deeplabcut/modelzoo/"), + case_insensitive_match("superanimal"), + # case_insensitive_match("modelzoo"), # too broad ? + ], + 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..) # noqa: E501 + ], + functional_scripts=[ + # 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( + name="multianimal", + match_any=[ + case_insensitive_match("multianimal"), + all_of(prefix(POSE_TF), case_insensitive_match("multi")), + all_of(prefix(POSE_PT), case_insensitive_match("multi")), + ], + pytest_paths=[ + "tests/test_auxfun_multianimal.py", + "tests/test_pose_multianimal_imgaug.py", + "tests/test_predict_multianimal.py", + "tests/test_stitcher.py", + "tests/test_trackingutils.py", + ], + functional_scripts=[ + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="core", + match_any=[ + prefix( + "deeplabcut/core/", + "deeplabcut/utils/", + POSE_TF, + POSE_PT, + ), + equals("deeplabcut/auxiliaryfunctions.py"), + ], + pytest_paths=[ + "tests/test_auxiliaryfunctions.py", + "tests/core/", + "tests/utils/", + ], + functional_scripts=[ + "examples/testscript_tensorflow_single_animal.py", + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="pose_estimation_tensorflow", + match_any=[ + prefix(POSE_TF), + ], + pytest_paths=[ + "tests/test_dataset_augmentation.py", + "tests/test_pose_multianimal_imgaug.py", + "tests/test_predict_multianimal.py", + "tests/test_evaluate.py", + # "tests/test_inferenceutils.py", + # "tests/test_crossvalutils.py", + ], + functional_scripts=[ + "examples/testscript_tensorflow_multi_animal.py", + "examples/testscript_tensorflow_single_animal.py", + ], + ), + CategoryRule( + name="pose_estimation_pytorch", + match_any=[ + prefix(POSE_PT), + ], + pytest_paths=[ + "tests/pose_estimation_pytorch/", + ], + functional_scripts=[ + "examples/testscript_pytorch_single_animal.py", + "examples/testscript_pytorch_multi_animal.py", + ], + ), + CategoryRule( + name="3d_pose_estimation", + match_any=[ + prefix("deeplabcut/pose_estimation_3d/"), + ], + pytest_paths=[ + "tests/test_triangulation.py", + ], + functional_scripts=[ + "examples/testscript_3d.py", + ], + ), + CategoryRule( + name="generate_training_dataset", + match_any=[ + prefix("deeplabcut/generate_training_dataset/"), + ], + pytest_paths=[ + "tests/generate_training_dataset/", + ], + functional_scripts=[], + ), + # CI & TOOLING # + # CategoryRule( + # name="ci_workflows", + # match_any=[ + # prefix(".github/workflows/"), + # ], + # pytest_paths=MINIMAL_PYTEST, + # functional_scripts=[], + # ), + CategoryRule( + name="ci_tools", + match_any=[ + prefix("tools/"), + ], + pytest_paths=["tests/tools/"], + functional_scripts=[], + ), + ] +) + +CATEGORY_RULE_BY_NAME = {r.name: r for r in CATEGORY_RULES} diff --git a/tools/trim_lines.py b/tools/trim_lines.py new file mode 100644 index 0000000000..f962b609e3 --- /dev/null +++ b/tools/trim_lines.py @@ -0,0 +1,101 @@ +#!/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 + +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 __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()) diff --git a/tools/update_license_headers.py b/tools/update_license_headers.py new file mode 100644 index 0000000000..ca5381deb2 --- /dev/null +++ b/tools/update_license_headers.py @@ -0,0 +1,63 @@ +"""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. +""" + +import fnmatch +import glob +import subprocess +import tempfile + +import yaml + + +def load_config(filename): + with open(filename) as fh: + config = yaml.safe_load(fh) + return config + + +def walk_directory(entry): + """Talk the directory.""" + + if "header" not in entry: + raise ValueError("Current entry does not have a header.") + if "include" not in entry: + raise ValueError("Current entry does not have an include list.") + + def _list_include(): + """List all files specified in the include list.""" + for include_pattern in entry["include"]: + yield from glob.iglob(include_pattern, recursive=True) + + def _filter_exclude(iterable): + """Filter filenames from an iterator by the exclude patterns.""" + for filename in iterable: + for exclude_pattern in entry.get("exclude", []): + if fnmatch.fnmatch(filename, exclude_pattern): + break + else: + yield filename + + files = _filter_exclude(set(_list_include())) + return list(files) + + +def main(input_file="NOTICE.yml"): + config = load_config(input_file) + for entry in config: + filelist = list(walk_directory(entry)) + with tempfile.NamedTemporaryFile(mode="w") as header_file: + header_file.write(entry["header"]) + header_file.flush() + header_file.seek(0) + command = ["licenseheaders", "-t", str(header_file.name), "-f"] + filelist + result = subprocess.run(command, capture_output=True) + if result.returncode != 0: + print(result.stdout.decode()) + print(result.stderr.decode()) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..d6da389dad --- /dev/null +++ b/uv.lock @@ -0,0 +1,9006 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +conflicts = [[ + { package = "deeplabcut", extra = "apple-mchips" }, + { package = "deeplabcut", extra = "tf" }, + { package = "deeplabcut", extra = "tf-cu11" }, + { package = "deeplabcut", extra = "tf-cu12" }, + { package = "deeplabcut", extra = "tf-latest" }, +], [ + { package = "deeplabcut", extra = "fmpose3d" }, + { package = "deeplabcut", extra = "tf-cu11" }, + { package = "deeplabcut", extra = "tf-cu12" }, +]] + +[manifest] + +[[manifest.dependency-metadata]] +name = "openvino-dev" +version = "2022.1.0" + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "albumentations" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pyyaml" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/4bb46e1afe56a4908bfe6fe58a9231ed5d7e7f46e400559d8bce7fc7dedc/albumentations-1.4.3.tar.gz", hash = "sha256:d0a5ca4edb1695693faa39cdf427d53fdbd7d26f7f8e4100c307e5ff5a463d16", size = 171598, upload-time = "2024-04-03T01:51:05.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/01/4202bd81ab337dca5693d7d1cb25c8e9041d97762aee738a24382ff9af2f/albumentations-1.4.3-py3-none-any.whl", hash = "sha256:386a7a61b6978f90163bf678745674f1d9031fd39cf17a40ae01d88aade72608", size = 137007, upload-time = "2024-04-03T01:51:02.83Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "app-model" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "in-n-out" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pydantic-compat" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/23/a9537fd7021e8b6ea7db96b73e66fe61e809a3b23338de07d1176e69e4ee/app_model-0.4.0.tar.gz", hash = "sha256:ccf667999f6c659e921ca3490b6da176971e67cf2f41abc34e33caa8cfa18573", size = 120529, upload-time = "2025-06-20T17:41:08.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/fa/d7f4ae02a3b115abdf561d05ba8d5cdf9e3b2b5724b45d91e6cf08c163c3/app_model-0.4.0-py3-none-any.whl", hash = "sha256:5b1d69ee023d955b0c7a525771fd2fc02ff9d82cd2f12a982949423c3a901210", size = 65710, upload-time = "2025-06-20T17:41:07.696Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "bleach" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "blosc2" +version = "4.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "ndindex" }, + { name = "numexpr", marker = "platform_machine != 'wasm32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "threadpoolctl", marker = "platform_machine != 'wasm32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/fb/c4d83eb40c8b9cb024507dbc4a3a91bfbc7e36ad5f2528c306575a87f42b/blosc2-4.3.1.tar.gz", hash = "sha256:f0c26b1190b24aae8b58ade95abd7c9aca5e7447db0f13c1915fcbe56a0b8167", size = 5354390, upload-time = "2026-05-19T17:34:45.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/a9/d952e141cea441a4387bf9c2e8917764569092e148eba2b3209408a0b655/blosc2-4.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c9bcd07ec550960513c6f9c2b901251c15d9bbfe79a554bbb9363162a92f5db", size = 5817410, upload-time = "2026-05-19T17:33:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/92/ab/fb94743b4511fb27accb4f9e3d5769cfd039d34801a7787b274cb6455c1c/blosc2-4.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b38dff8a6d3446f08c7b25c8a00d63da32b63a35c05561d6be77bb67cf352de", size = 5026379, upload-time = "2026-05-19T17:33:56.8Z" }, + { url = "https://files.pythonhosted.org/packages/81/df/a1b9bcb00d028f69fc58e45702d99d86a4eddf78640cd4ad87b2ab7cfa3d/blosc2-4.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:50ec0dec76b75411faaa7e79d2fb5adbfdd90b8b90c0637faa706d03336c87d9", size = 6306638, upload-time = "2026-05-19T17:33:59.029Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/b2515f08969c0ed5dd55337ca11dcf9034e2b23250b16badbe6538b27021/blosc2-4.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a52b29306ff7e5b49f8365a7f9567c8bda9a1d3b2e042a4ca185268a379e05d", size = 6589405, upload-time = "2026-05-19T17:34:01.047Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/1874f6b0ffec99ba908e99ec547e4d45f3740b5c03f8ab1f310a3e36e22c/blosc2-4.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:320c9d6ba943aab3570f15d413c4c8b634bbd9d70c00199bf2cf74fe01112f71", size = 4126788, upload-time = "2026-05-19T17:34:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/1d/29/f685930f492ec787557635b5e0e91b229c793cd3fd3d32083e5f088a15b9/blosc2-4.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a1d8d56a7bbd942dc9f46b2c78178b16075ff5e88d7fb466c96e6fd0ff7fcd6", size = 5811703, upload-time = "2026-05-19T17:34:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/3a/24/fc29520a463b96acf64d3fdf5c8bd6fd5d2a11dbda6d08af8fb69de8611b/blosc2-4.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93a02d9f11e4308385f4f556a14573e24310d49042f6d9ea61d74b9c42880980", size = 5021613, upload-time = "2026-05-19T17:34:05.941Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/d80c3e198e056eec0aed87065a68cead2e8c17cffbf8d6aad498d37946cc/blosc2-4.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6759c659467dcf28c7e5d72bf90b7e5559d5e17533364461ab69ab85cce3d4f2", size = 6295416, upload-time = "2026-05-19T17:34:07.722Z" }, + { url = "https://files.pythonhosted.org/packages/17/ba/8a5d0dcfb95126b6601e4f993ee276c9953be4748bd67f45202fee4ee58e/blosc2-4.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:706c27f5b93df7e320b1cdbc446f248d027106f968f762ecb240fde98bbe8af1", size = 6589973, upload-time = "2026-05-19T17:34:09.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/4b/a6fe2d0a496cf467c205cf83993c605dd38422a86adbe710fa2f8f5bcdf5/blosc2-4.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:95c8466beebef66c24ef2dd1993cda3c1f8c3c00ec3be0dcdc098dc5eb6b259c", size = 4124211, upload-time = "2026-05-19T17:34:11.07Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c7db23a891a5eb1893c1b3da206810db48d44cc0180e1b737dab1d5f5733/blosc2-4.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d5aa1e54412f45da894b022fa33387361332884a6689af4121dad1d91c205fa", size = 5857286, upload-time = "2026-05-19T17:34:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/04a147753b4e7fcf227f7a4c6480e3ee545909529d038bbd8a2e0111bd4a/blosc2-4.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf349b41af997c775a94e02af45a4215e8ff3d8ff5e2c8e29b452d697e2c3a90", size = 5020423, upload-time = "2026-05-19T17:34:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/44/22/8f53101763841e907456e92ae06e5318f87228e6fb4e7aa28134c3abee2f/blosc2-4.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a162b378741233170fee4fee213f33b03799d4be42603a3f8344f5ed0df9f32a", size = 6240864, upload-time = "2026-05-19T17:34:16.522Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/1c8818d8afd691dfbd847fb478bb606042114fdbdb321a8a12d0e707dd01/blosc2-4.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa6f6ea348ada734bf6d5c5b6988d47b975ad42fb160952ed92c6b0e8801e86", size = 6539023, upload-time = "2026-05-19T17:34:17.868Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/0ecbef3077298fe4f9daf9c6c8ece80a0f98d1c6efc56e71cc45ad158c75/blosc2-4.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:2cedea2ac373b64b00aa956a04f74d885bdef033878e8cf84e53c001bbef6dc8", size = 4121473, upload-time = "2026-05-19T17:34:19.57Z" }, + { url = "https://files.pythonhosted.org/packages/01/a9/b363f846afb1fec3b49acc4a248aa98bc31c55370effc59c455caa5398ad/blosc2-4.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fae6ed1deb9e79bd703d959e9c4f5a12d6cbfdb2b3713f646ad0f7266c96c114", size = 5855462, upload-time = "2026-05-19T17:34:21.179Z" }, + { url = "https://files.pythonhosted.org/packages/60/6f/48ad9373d2d7fb82321f177a4525cccece70a996242c0a2205160aa9c87d/blosc2-4.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87c0f387b8d47d4e6e1f27c72d92366ef16ceb1b80629ed5eaab241822c386af", size = 5018781, upload-time = "2026-05-19T17:34:23.04Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2f/5777d61be67c001d7674e00302350ba7a288cfea706c5181d440b0d2e8d5/blosc2-4.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c48881de2fe195084a1c583acb4ab66ee8b2f0b8f83fba00148a9eb06f4826fe", size = 6248194, upload-time = "2026-05-19T17:34:24.748Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d6/9ecbfbb73daf8b95149e48dbca9f84d6303e6fac77e3e635c09f281f6f79/blosc2-4.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce881214846959a375279ac2f7ab66e2bfa868bfbd01188ae1329c243f6d1275", size = 6541927, upload-time = "2026-05-19T17:34:26.219Z" }, + { url = "https://files.pythonhosted.org/packages/be/a7/40ead062061388ae6ac4981fa93127a7ed2da2ca66b2a9c09b191f00b7b1/blosc2-4.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:091202d20394e5593a365950cc43b09c49ececdcac36d3b491ddecc3600f36c2", size = 4121405, upload-time = "2026-05-19T17:34:27.615Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/a9b75f9a228e9ac3a202d23d8d8fa23d737064919ac2a3c3866cfaa80b5d/blosc2-4.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0c50a9c64cad0228e382a09e605c4fd6cd9aa2576b76ab16005121a4a57f9698", size = 5857264, upload-time = "2026-05-19T17:34:29.317Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/3561e51ed30c26b6272675b4528457b44c840af11b5b0741fbd9d9d62287/blosc2-4.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1eb44f6ff517c6e3476a3eb3a85b357e4604bd3274db354bf52f854fb50bf", size = 5021316, upload-time = "2026-05-19T17:34:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/12/6c/deff0f7314be3af1cf2fc728bae0be7414084b1ff51cb9a61c5a54997742/blosc2-4.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a31ae1e372f2f53215bc8bcecfe4aa5af20a8cfe4124dccc8dc334093539b2c9", size = 6260060, upload-time = "2026-05-19T17:34:32.648Z" }, + { url = "https://files.pythonhosted.org/packages/44/56/f941a447f47e3c997acb3ff1459c8a26e08f383ec60adf80e3ce04de6be3/blosc2-4.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565e4d038089c495e06b2e3e5a2881d61619460c8448c0291dbd84840437c28d", size = 6543150, upload-time = "2026-05-19T17:34:34.694Z" }, + { url = "https://files.pythonhosted.org/packages/77/15/8a91f03a84854615dfa0b2b7afd9ee29b0b3bde5e28ebe38f5f49de438e1/blosc2-4.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d76a639d7287bb3fec73dd1ae8196923d95e00b95069bce3c495eaa00acdfe8", size = 4204741, upload-time = "2026-05-19T17:34:36.095Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/a246dd2f7ef538a572f61bfeab53d6ae0bcdf3e8cc5187c284189f7f27dc/blosc2-4.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:365235f674f3b70ae2eae3e2e7f0cdbda7b5e02fa50b357ab211dd45b91ad386", size = 5895602, upload-time = "2026-05-19T17:34:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d0/40a9ac3634b520a9a9ffe30569b7b1296fd5e337c858883f379a6844f4c8/blosc2-4.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbd29c7f7d2fdaf164f81f68afa2580941b9832ace77f61c01c39788a48b67f7", size = 5067186, upload-time = "2026-05-19T17:34:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/91/6a/4607099251c65db83040686dfb6f1711278b536725552611365967d978e5/blosc2-4.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c558251376af56dea742ab60233faa1c52381a5f1b1a3db20dce192d3e8e7fd8", size = 6222994, upload-time = "2026-05-19T17:34:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bd/a9f95816a1a680ddd0f18f2d422df98ee4033f59db71960976e66abda361/blosc2-4.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cde49a4cf3b207d36abfeb963eccac998e34b930603baff068cd3593b2d9631f", size = 6512973, upload-time = "2026-05-19T17:34:42.135Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0e/b1669555dbe381344e97056c9c5c4c9bbf2111026d2f2aa660059bbc541f/blosc2-4.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3424327e0ae91e98b9d9c3a32cc27e76d82dc0ec193e81e5b6c770bce5767e7c", size = 4264125, upload-time = "2026-05-19T17:34:43.931Z" }, +] + +[[package]] +name = "bottleneck" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/38/144fb32c9efb196f651ddb30e7c48f6047a86972e5b350f3f10c9a5f6a16/bottleneck-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40de6be68218ba32cd15addbf4ad7bbbf0075b5c5c4347c579aeae110a5c9a96", size = 100393, upload-time = "2025-09-08T16:29:35.466Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e3/dbbf4b102f4e6aaf49ad3749a6d778f309473a2950c5ce3bb20b94f2ba84/bottleneck-1.6.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ad1882ba8c8da1f404de2610b45b05291e39eec56150270b03b5b25cf2bbb7f", size = 371509, upload-time = "2025-09-08T16:29:37.037Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/60fcbddee5fdf32923ba33ce2337a4cf12834b69de4f8e07219b5ef7c931/bottleneck-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f29b14b0ba5a816df6ab559add415c88ea8cf2146364e55f5f4c24ff7c85e494", size = 363480, upload-time = "2025-09-08T16:29:38.311Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/a25434dcadf083e05b0c71ece2de71fad5521268f905721e06e0a7efc5db/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17c227ed361cf9a2ab3751a727620298faca9a1e33dd76711ae80834cf34b254", size = 357120, upload-time = "2025-09-08T16:29:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b9/99580349c827695dfc094ac672eedba6e1ca244b6e745ff7447c0239d6d8/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d278b5633cea38bdae6eaf7df23d54ecb5e4db52f2ebc13fe40c0e738842f2a1", size = 367579, upload-time = "2025-09-08T16:29:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/6326994249388ceb2400d07c6a96a50941749d2d9ec80da22a99046e3a38/bottleneck-1.6.0-cp310-cp310-win32.whl", hash = "sha256:26c87c2f6364d82b67eab7218f0346e9c42f336088ca4e19d77dc76eecf272fc", size = 107838, upload-time = "2025-09-08T16:29:41.907Z" }, + { url = "https://files.pythonhosted.org/packages/2f/75/8f0e8e266ea99ffbc69500a927f0c114a07fe465bfbc59871d6fe22d9ee0/bottleneck-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d33bcd60a13d0603f5db9d953352a3c098242c46f8f919290fd11c54b42b9e5", size = 113364, upload-time = "2025-09-08T16:29:43.437Z" }, + { url = "https://files.pythonhosted.org/packages/83/96/9d51012d729f97de1e75aad986f3ba50956742a40fc99cbab4c2aa896c1c/bottleneck-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:69ef4514782afe39db2497aaea93b1c167ab7ab3bc5e3930500ef9cf11841db7", size = 100400, upload-time = "2025-09-08T16:29:44.464Z" }, + { url = "https://files.pythonhosted.org/packages/16/f4/4fcbebcbc42376a77e395a6838575950587e5eb82edf47d103f8daa7ba22/bottleneck-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:727363f99edc6dc83d52ed28224d4cb858c07a01c336c7499c0c2e5dd4fd3e4a", size = 375920, upload-time = "2025-09-08T16:29:45.52Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/7fa8cdc41cbf2dfe0540f98e1e0caf9ffbd681b1a0fc679a91c2698adaf9/bottleneck-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:847671a9e392220d1dfd2ff2524b4d61ec47b2a36ea78e169d2aa357fd9d933a", size = 367922, upload-time = "2025-09-08T16:29:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/13/7d/dccfa4a2792c1bdc0efdde8267e527727e517df1ff0d4976b84e0268c2f9/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:daef2603ab7b4ec4f032bb54facf5fa92dacd3a264c2fd9677c9fc22bcb5a245", size = 361379, upload-time = "2025-09-08T16:29:48.042Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/21c0fad823b71c3a8904cbb847ad45136d25573a2d001a9cff48d3985fab/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc7f09bda980d967f2e9f1a746eda57479f824f66de0b92b9835c431a8c922d4", size = 371911, upload-time = "2025-09-08T16:29:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/830ff80f8c74577d53034c494639eac7a0ffc70935c01ceadfbe77f590c2/bottleneck-1.6.0-cp311-cp311-win32.whl", hash = "sha256:1f78bad13ad190180f73cceb92d22f4101bde3d768f4647030089f704ae7cac7", size = 107831, upload-time = "2025-09-08T16:29:51.397Z" }, + { url = "https://files.pythonhosted.org/packages/6f/42/01d4920b0aa51fba503f112c90714547609bbe17b6ecfc1c7ae1da3183df/bottleneck-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f2adef59fdb9edf2983fe3a4c07e5d1b677c43e5669f4711da2c3daad8321ad", size = 113358, upload-time = "2025-09-08T16:29:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/8d/72/7e3593a2a3dd69ec831a9981a7b1443647acb66a5aec34c1620a5f7f8498/bottleneck-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bb16a16a86a655fdbb34df672109a8a227bb5f9c9cf5bb8ae400a639bc52fa3", size = 100515, upload-time = "2025-09-08T16:29:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/e7bbea08f4c0f0bab819d38c1a613da5f194fba7b19aae3e2b3a27e78886/bottleneck-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fbf5d0787af9aee6cef4db9cdd14975ce24bd02e0cc30155a51411ebe2ff35f", size = 377451, upload-time = "2025-09-08T16:29:56.718Z" }, + { url = "https://files.pythonhosted.org/packages/fe/80/a6da430e3b1a12fd85f9fe90d3ad8fe9a527ecb046644c37b4b3f4baacfc/bottleneck-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d08966f4a22384862258940346a72087a6f7cebb19038fbf3a3f6690ee7fd39f", size = 368303, upload-time = "2025-09-08T16:29:57.834Z" }, + { url = "https://files.pythonhosted.org/packages/30/11/abd30a49f3251f4538430e5f876df96f2b39dabf49e05c5836820d2c31fe/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:604f0b898b43b7bc631c564630e936a8759d2d952641c8b02f71e31dbcd9deaa", size = 361232, upload-time = "2025-09-08T16:29:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ac/1c0e09d8d92b9951f675bd42463ce76c3c3657b31c5bf53ca1f6dd9eccff/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d33720bad761e642abc18eda5f188ff2841191c9f63f9d0c052245decc0faeb9", size = 373234, upload-time = "2025-09-08T16:30:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/382c572ae3057ba885d484726bb63629d1f63abedf91c6cd23974eb35a9b/bottleneck-1.6.0-cp312-cp312-win32.whl", hash = "sha256:a1e5907ec2714efbe7075d9207b58c22ab6984a59102e4ecd78dced80dab8374", size = 108020, upload-time = "2025-09-08T16:30:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493, upload-time = "2025-09-08T16:30:02.872Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/e117cd5ff7056126d3291deb29ac8066476e60b852555b95beb3fc9d62a0/bottleneck-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015de414ca016ebe56440bdf5d3d1204085080527a3c51f5b7b7a3e704fe6fd", size = 100521, upload-time = "2025-09-08T16:30:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:456757c9525b0b12356f472e38020ed4b76b18375fd76e055f8d33fb62956f5e", size = 377719, upload-time = "2025-09-08T16:30:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/76593af47097d9633109bed04dbcf2170707dd84313ca29f436f9234bc51/bottleneck-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c65254d51b6063c55f6272f175e867e2078342ae75f74be29d6612e9627b2c0", size = 368577, upload-time = "2025-09-08T16:30:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/4dcacaf637d2b8d89ea746c74159adda43858d47358978880614c3fa4391/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a172322895fbb79c6127474f1b0db0866895f0b804a18d5c6b841fea093927fe", size = 361441, upload-time = "2025-09-08T16:30:07.613Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/21eb1eb1c42cb7be2872d0647c292fc75768d14e1f0db66bf907b24b2464/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5e81b642eb0d5a5bf00312598d7ed142d389728b694322a118c26813f3d1fa9", size = 373416, upload-time = "2025-09-08T16:30:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/48/cb/7957ff40367a151139b5f1854616bf92e578f10804d226fbcdecfd73aead/bottleneck-1.6.0-cp313-cp313-win32.whl", hash = "sha256:543d3a89d22880cd322e44caff859af6c0489657bf9897977d1f5d3d3f77299c", size = 108029, upload-time = "2025-09-08T16:30:09.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/735df4156fa5595501d5d96a6ee102f49c13d2ce9e2a287ad51806bc3ba0/bottleneck-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:48a44307d604ceb81e256903e5d57d3adb96a461b1d3c6a69baa2c67e823bd36", size = 113497, upload-time = "2025-09-08T16:30:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/8c1260df8ade7cebc2a8af513a27082b5e36aa4a5fb762d56ea6d969d893/bottleneck-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547e6715115867c4657c9ae8cc5ddac1fec8fdad66690be3a322a7488721b06b", size = 101606, upload-time = "2025-09-08T16:30:11.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/f03e2944e91ee962922c834ed21e5be6d067c8395681f5dc6c67a0a26853/bottleneck-1.6.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e4a4a6e05b6f014c307969129e10d1a0afd18f3a2c127b085532a4a76677aef", size = 391804, upload-time = "2025-09-08T16:30:13.13Z" }, + { url = "https://files.pythonhosted.org/packages/0b/58/2b356b8a81eb97637dccee6cf58237198dd828890e38be9afb4e5e58e38e/bottleneck-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2baae0d1589b4a520b2f9cf03528c0c8b20717b3f05675e212ec2200cf628f12", size = 383443, upload-time = "2025-09-08T16:30:14.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/cf7d09ed3736ad0d50c624787f9b580ae3206494d95cc0f4814b93eef728/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2e407139b322f01d8d5b6b2e8091b810f48a25c7fa5c678cfcdc420dfe8aea0a", size = 375458, upload-time = "2025-09-08T16:30:15.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/7c87a34a24e339860064f20fac49f6738e94f1717bc8726b9c47705601d8/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adefb89b92aba6de9c6ea871d99bcd29d519f4fb012cc5197917813b4fc2c7f", size = 386384, upload-time = "2025-09-08T16:30:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/59/57/db51855e18a47671801180be748939b4c9422a0544849af1919116346b5f/bottleneck-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:64b8690393494074923780f6abdf5f5577d844b9d9689725d1575a936e74e5f0", size = 109448, upload-time = "2025-09-08T16:30:18.076Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/683c090b624f13a5bf88a0be2241dc301e98b2fb72a45812a7ae6e456cc4/bottleneck-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:cb67247f65dcdf62af947c76c6c8b77d9f0ead442cac0edbaa17850d6da4e48d", size = 115190, upload-time = "2025-09-08T16:30:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/eb7c08964a3f3c4719f98795ccd21807ee9dd3071a0f9ad652a5f19196ff/bottleneck-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1d789042511a0f042b3bdcd2903e8567e956d3aa3be189cce3746daeb8550", size = 100544, upload-time = "2025-09-08T16:30:20.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/ec/c6f3be848f37689f481797ce7d9807d5f69a199d7fc0e46044f9b708c468/bottleneck-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fad24c99e39ad7623fc2a76d37feb26bd32e4dd170885edf4dbf4bfce2199a3", size = 378315, upload-time = "2025-09-08T16:30:21.409Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/2d6600836e2ea8f14fcefac592dc83497e5b88d381470c958cb9cdf88706/bottleneck-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643e61e50a6f993debc399b495a1609a55b3bd76b057e433e4089505d9f605c7", size = 368978, upload-time = "2025-09-08T16:30:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/bf72b49f5040212873b985feef5050015645e0a02204b591e1d265fc522a/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa668efbe4c6b200524ea0ebd537212da9b9801287138016fdf64119d6fcf201", size = 362074, upload-time = "2025-09-08T16:30:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c8/c4891a0604eb680031390182c6e264247e3a9a8d067d654362245396fadf/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f7dd35262e89e28fedd79d45022394b1fa1aceb61d2e747c6d6842e50546daa", size = 374019, upload-time = "2025-09-08T16:30:26.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/ed096f8d1b9147e84914045dd89bc64e3c32eee49b862d1e20d573a9ab0d/bottleneck-1.6.0-cp314-cp314-win32.whl", hash = "sha256:bd90bec3c470b7fdfafc2fbdcd7a1c55a4e57b5cdad88d40eea5bc9bab759bf1", size = 110173, upload-time = "2025-09-08T16:30:27.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/70/1414acb6ae378a15063cfb19a0a39d69d1b6baae1120a64d2b069902549b/bottleneck-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:b43b6d36a62ffdedc6368cf9a708e4d0a30d98656c2b5f33d88894e1bcfd6857", size = 115899, upload-time = "2025-09-08T16:30:28.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ed/4570b5d8c1c85ce3c54963ebc37472231ed54f0b0d8dbb5dde14303f775f/bottleneck-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:53296707a8e195b5dcaa804b714bd222b5e446bd93cd496008122277eb43fa87", size = 101615, upload-time = "2025-09-08T16:30:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/c148faa07ae91f266be1f3fad1fde95aa2449e12937f3f3df2dd720b86e0/bottleneck-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6df19cc48a83efd70f6d6874332aa31c3f5ca06a98b782449064abbd564cf0e", size = 392411, upload-time = "2025-09-08T16:30:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/e6ad221d345a059e7efb2ad1d46a22d9fdae0486faef70555766e1123966/bottleneck-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96bb3a52cb3c0aadfedce3106f93ab940a49c9d35cd4ed612e031f6deb27e80f", size = 384022, upload-time = "2025-09-08T16:30:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/40/5b15c01eb8c59d59bc84c94d01d3d30797c961f10ec190f53c27e05d62ab/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1db9e831b69d5595b12e79aeb04cb02873db35576467c8dd26cdc1ee6b74581", size = 376004, upload-time = "2025-09-08T16:30:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/cb228f5949553a5c01d1d5a3c933f0216d78540d9e0bf8dd4343bb449681/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4dd7ac619570865fcb7a0e8925df418005f076286ad2c702dd0f447231d7a055", size = 386909, upload-time = "2025-09-08T16:30:34.973Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/425065c37a67a9120bf53290371579b83d05bf46f3212cce65d8c01d470a/bottleneck-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:7fb694165df95d428fe00b98b9ea7d126ef786c4a4b7d43ae2530248396cadcb", size = 111636, upload-time = "2025-09-08T16:30:36.044Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (os_name != 'nt' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "cachey" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "heapdict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/9c/e3c959c1601013bf8a72e8bf91ea1ebc6fe8a2305bd2324b039ee0403277/cachey-0.2.1.tar.gz", hash = "sha256:0310ba8afe52729fa7626325c8d8356a8421c434bf887ac851e58dcf7cf056a6", size = 6461, upload-time = "2020-03-11T15:34:08.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/f0/e24f3e5d5d539abeb783087b87c26cfb99c259f1126700569e000243745a/cachey-0.2.1-py3-none-any.whl", hash = "sha256:49cf8528496ce3f99d47f1bd136b7c88237e55347a15d880f47cefc0615a83c3", size = 6415, upload-time = "2020-03-11T15:34:07.347Z" }, +] + +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/07/e8412a13019b3f737972dea23a2c61ca42becafc16c9338f4ca7a0caa993/cairosvg-2.9.0.tar.gz", hash = "sha256:1debb00cd2da11350d8b6f5ceb739f1b539196d71d5cf5eb7363dbd1bfbc8dc5", size = 40877, upload-time = "2026-03-13T15:42:00.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cmake" +version = "4.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/07/f1d6f7bcf056a139352cc2972f92a92005ac0ee98103165b1f620873b196/cmake-4.3.2.tar.gz", hash = "sha256:5f47f5f00910c474662d09a0516413c6e9750bde73cdc52dea3988102a274e06", size = 36969, upload-time = "2026-04-23T21:51:35.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/15/4c8980d5ceef53f0c490425f5b8e47f3ff863348400b0ea5ba4e349119f1/cmake-4.3.2-py3-none-macosx_10_10_universal2.whl", hash = "sha256:f8f570813753ed4564928cf45c4c13c31e46b3e66b1a07fe695cb9f7b7af185e", size = 52883814, upload-time = "2026-04-23T21:50:16.764Z" }, + { url = "https://files.pythonhosted.org/packages/bc/50/2f336143dbcf5eca7c2d7e86273a84ef60231a0b7cfa33143d964c62f250/cmake-4.3.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ca739ab0d8960261fdb1bb6e1e6c16b9cd033ae0a98341483cc233ba7c81b22b", size = 29578008, upload-time = "2026-04-23T21:50:21.965Z" }, + { url = "https://files.pythonhosted.org/packages/52/e9/4b571a24924b5bdbd5c0b0fbb0b6b1008eb6472ceb9f23bca9e08e09632e/cmake-4.3.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b81038453a40aed73f8d28185c4c3ef43c3a91a7ff1577ce08e498769ecfe16", size = 30676971, upload-time = "2026-04-23T21:50:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d4/87f91ba2030c862a27a5b2df42e4e80d3244910acd10aa4cdcd0111820a9/cmake-4.3.2-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8add046a4ef7c606d8e0a444050415054c7da3b8535b2c8ce1f03e265dda098d", size = 30462837, upload-time = "2026-04-23T21:50:28.785Z" }, + { url = "https://files.pythonhosted.org/packages/21/ee/5475cd861db5e8e22fd6b3fd323a67f8f62df487163f72bc281739309bcf/cmake-4.3.2-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:7105a5411bbc405242677d29222812028566aafac3f78fda08c1717f03e5ca2a", size = 28377235, upload-time = "2026-04-23T21:50:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/63/cd/1008be054420fd759b73d3328326d047ea693c3910a55cb2a597d911baee/cmake-4.3.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:339655b93289c1b03c6a72523d46d3b0d19dc51406d3a90f8eefcbec525cb271", size = 29512079, upload-time = "2026-04-23T21:50:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/13/c4/e7c3649c4941927aff24c464090c4ce7f1f24167077f1b8aff3994def88d/cmake-4.3.2-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:ea95db137fd27f420d5e149c41e1f7621e786869afa3ca0fe18301df9e066607", size = 26628649, upload-time = "2026-04-23T21:50:40.047Z" }, + { url = "https://files.pythonhosted.org/packages/5a/05/ee3b002e0e7303a36e1f3c52e18d004fe089076a0e7b38df5e64a2328e88/cmake-4.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3947bc5973ca3846c76486993abd0fba0cb9119300d58ed9173a672d1eef505b", size = 26769012, upload-time = "2026-04-23T21:50:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/4e42625ce96197ffd72bb6a0e9c02d64506ee3075dd801219bb44fd9dcf7/cmake-4.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bc316be89fa43c265697c3b9ffcebde977bcc4515974372cc460c974f458ff98", size = 38595561, upload-time = "2026-04-23T21:50:48.601Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/80ab6fa7aceff0af186a844b6a53c3022e9775467804a1ce2ec3aafb02b3/cmake-4.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d10648929eb3294449ceae7a7c0b9714ca445280ddc25c209f427e7a07c6f3a9", size = 35226159, upload-time = "2026-04-23T21:50:53.968Z" }, + { url = "https://files.pythonhosted.org/packages/10/e3/73fc202fb943221a7ce78a5374c7a73093d26af85378ddab9c04586dc98b/cmake-4.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a69e629e6cd973e1f9b11d247d116acb47b35cdd8e39aee4b04b9040851cd8a0", size = 41262833, upload-time = "2026-04-23T21:50:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/3ef79820bfbcd5b51ef3b8029fc9c96da363daa11aae02ddd82df08fa446/cmake-4.3.2-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:95e0ff31a692d12130f33d1467d0074f8314cf3a79940694896de9367b6e4fae", size = 40437896, upload-time = "2026-04-23T21:51:04.304Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/8a1147fa26a3c1564bf654b744baeb2b8c0efccfde04190d3f9222c27bde/cmake-4.3.2-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:67775407b963385a7942dd56f0567ef3c75453c6336654aeafe43165d78648bb", size = 35492662, upload-time = "2026-04-23T21:51:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/42a744beb4c9f6e459f1a57162f391a9941f31850048fc461e88095f98da/cmake-4.3.2-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:aae70bb95762f6da20131cf72da6322557ff6784968755d201d42a44cd9494e5", size = 37723238, upload-time = "2026-04-23T21:51:13.595Z" }, + { url = "https://files.pythonhosted.org/packages/06/4c/513a73685feef886c824982b6618bb3cc7e7ac8579b34f1fe043bce11af5/cmake-4.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1c5c292b1189e48d01f0bed01ed800c31eed967afd033c4beabff9cc97209f2", size = 38554155, upload-time = "2026-04-23T21:51:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/14/aa/35b387f6cc0990247195109b04523a1c05dbb4f0fd0bae28e7f34c4a1e6f/cmake-4.3.2-py3-none-win32.whl", hash = "sha256:3c55f0c61c70642d9e7f6b4fc638622027f045b388e357d74efcca4a7111e4aa", size = 37819702, upload-time = "2026-04-23T21:51:22.933Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/7ceb7223d274e88d621ce00f2160ae74aead18a3d36f61b8fb52cbe6b7ca/cmake-4.3.2-py3-none-win_amd64.whl", hash = "sha256:78049aac277aabe376d8e82993f0be234d086d0e8ad4708755d5a209a04e1138", size = 41272507, upload-time = "2026-04-23T21:51:27.93Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/fa4da5330b63d5ce7909892005eced21d1fca58d022ed6b40f216f6f6c52/cmake-4.3.2-py3-none-win_arm64.whl", hash = "sha256:b218d636a99fa0eb23713d37d3e3c3a9c0e707e0579b46780ff908acef229386", size = 39613878, upload-time = "2026-04-23T21:51:33.041Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "(python_full_version < '3.12' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and platform_python_implementation != 'PyPy' and extra == 'extra-10-deeplabcut-tf') or (platform_python_implementation != 'PyPy' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/31/bfcc870f69c6a017c4ad5c42316207fc7551940db6f3639aa4466ec5faf3/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a022c96b8bd847e8dc0675523431149a4c3e872f440e3002213dbb9e08f0331a", size = 11800959, upload-time = "2025-10-21T14:51:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1e/9c8ed3f3dbed7b7d038805fdc65cbc65fda9983e84437778a9571e7092bc/cuda_bindings-12.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:f69107389e6b9948969bfd0a20c4f571fd1aefcfb1d2e1b72cc8ba5ecb7918ab", size = 11464568, upload-time = "2025-10-21T14:51:31.454Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2b/ebcbb60aa6dba830474cd360c42e10282f7a343c0a1f58d24fbd3b7c2d77/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306", size = 11840604, upload-time = "2025-10-21T14:51:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/dd/be/90d32049e06abcfba4b2e7df1dbcb5e16215c8852eef0cd8b25f38a66bd4/cuda_bindings-12.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913", size = 11490933, upload-time = "2025-10-21T14:51:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/ec/07/6aff13bc1e977e35aaa6b22f52b172e2890c608c6db22438cf7ed2bf43a6/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d", size = 11566797, upload-time = "2025-10-21T14:51:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3c/972edfddb4ae8a9fccd3c3766ed47453b6f805b6026b32f10209dd4b8ad4/cuda_bindings-12.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c", size = 11894363, upload-time = "2025-10-21T14:51:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b5/96a6696e20c4ffd2b327f54c7d0fde2259bdb998d045c25d5dedbbe30290/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5", size = 11624530, upload-time = "2025-10-21T14:52:01.539Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/e6/87/652796522cc1a7af559460e1ce59b642e05c1468b9c08522a9a096b4cf04/cuda_bindings-12.9.4-cp314-cp314-win_amd64.whl", hash = "sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575", size = 11517716, upload-time = "2025-10-21T14:52:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/39/73/d2fc40c043bac699c3880bf88d3cebe9d88410cd043795382826c93a89f0/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f", size = 11565056, upload-time = "2025-10-21T14:52:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { url = "https://files.pythonhosted.org/packages/ab/52/a30f46e822bfa6b4a659d1e8de8c4a4adf908ea075dac568b55362541bd8/cuda_bindings-12.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43", size = 12055608, upload-time = "2025-10-21T14:52:12.335Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-pathfinder", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ea/81999d01375645f34596c76eb046b4b36d58cc6fe2bddb2410f8a7b7a827/cuda_bindings-13.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:845025438a1b9e20718b9fb42add3e0eb72e85458bcab3eeb80bfd8f0a9dab33", size = 5600047, upload-time = "2026-03-11T00:12:34.848Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/29/5a/0ce1731c48bcd9f40996a4ef1abbf634f1a7fe4a15c5050b1e75ce3a7acf/cuda_bindings-13.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:debb51b211d246f8326f6b6e982506a5d0d9906672c91bc478b66addc7ecc60a", size = 5631363, upload-time = "2026-03-11T00:12:41.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a5/d7f01a415e134546248cef612adad8153c9f1eb10ec79505a7cd8294370b/cuda_bindings-13.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:45815daeb595bf3b405c52671a2542b1f8e9329f3b029494acbfcc74aeaa1f2d", size = 5840830, upload-time = "2026-03-11T00:12:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c4/84/d3b6220b51cbc02ca14db7387e97445126b4ff5125aaa6c5dd7dcb75e679/cuda_bindings-13.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8cebe3ce4aeeca5af9c490e175f76c4b569bbf4a35a62294b777bc77bf7ac4d8", size = 5796512, upload-time = "2026-03-11T00:12:54.483Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/e3/73/98bcb069778fe420226db75aff54b5dd6c3ecfd0912edabab723326e80b7/cuda_bindings-13.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd658bb5c0e55b7b3e5dd0ed509c6addb298c665db26a9bfba35e1e626000ba2", size = 5938605, upload-time = "2026-03-11T00:13:01.639Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/4e01cc06447d39476e138d1b1adec8d35c0d04eccd2c8d69befc08cd66e8/cuda_bindings-13.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ccf14e0c1def3b7200100aafff3a9f7e210ecb6e409329e92dcf6cd2c00d5c7", size = 6662637, upload-time = "2026-03-11T00:13:07.881Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dask" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/2a/5d8cc1579590af86576dde890254440e478c7174b93a02095ecfc2e6ba38/dask-2026.3.0.tar.gz", hash = "sha256:f7d96c8274e8a900d217c1ff6ea8d1bbf0b4c2c21e74a409644498d925eb8f85", size = 11000710, upload-time = "2026-03-18T07:10:14.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl", hash = "sha256:be614b9242b0b38288060fb2d7696125946469c98a1c30e174883fd199e0428d", size = 1485630, upload-time = "2026-03-18T07:10:12.832Z" }, +] + +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] +dataframe = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, +] + +[[package]] +name = "dask-image" +version = "2025.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask", extra = ["array", "dataframe"] }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pims" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/c4/7b83217443201469384a415687a8b89da8e55fc7a182e9507a69851a78b9/dask_image-2025.11.0.tar.gz", hash = "sha256:45cf1a9c3a8a1c143c75d43f1494e4fe0827564d3ec6efb93618fb04603ba0b3", size = 79561, upload-time = "2025-11-13T01:57:28.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/4b/817da308fa1170da07ef01259585887a3bbb6ab80700b3e61ce4967301ec/dask_image-2025.11.0-py3-none-any.whl", hash = "sha256:4834ece8d7133f8cd7d4e672f7f5a598c9057e687b20f14f3121360e3e1690b4", size = 61936, upload-time = "2025-11-13T01:57:27.133Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "deeplabcut" +version = "3.0.1" +source = { editable = "." } +dependencies = [ + { name = "albumentations" }, + { name = "dlclibrary" }, + { name = "einops" }, + { name = "filelock" }, + { name = "filterpy" }, + { name = "h5py", marker = "sys_platform == 'darwin' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "huggingface-hub" }, + { name = "imageio-ffmpeg" }, + { name = "imgaug" }, + { name = "matplotlib" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas", extra = ["hdf5", "performance"] }, + { name = "pillow" }, + { name = "pycocotools" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "ruamel-yaml" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "statsmodels" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "timm" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.28.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "tqdm" }, +] + +[package.optional-dependencies] +apple-mchips = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack", marker = "sys_platform == 'darwin'" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim", marker = "sys_platform == 'darwin'" }, +] +dev-docs = [ + { name = "black" }, + { name = "mike" }, + { name = "mkdocs" }, + { name = "mkdocs-api-autonav" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material", extra = ["imaging"] }, + { name = "mkdocstrings", extra = ["python"] }, +] +docs = [ + { name = "jupyter-book" }, + { name = "sphinxcontrib-mermaid" }, +] +fmpose3d = [ + { name = "fmpose3d" }, +] +gui = [ + { name = "napari-deeplabcut" }, + { name = "pyside6" }, + { name = "qdarkstyle" }, +] +modelzoo = [ + { name = "huggingface-hub" }, +] +openvino = [ + { name = "openvino-dev" }, +] +tf = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim" }, +] +tf-cu11 = [ + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.14.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'win32'" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.14.1", source = { registry = "https://pypi.org/simple" } }, + { name = "tf-slim" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" } }, +] +tf-cu12 = [ + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow", version = "2.18.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.18.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tf-slim" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" } }, +] +tf-latest = [ + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin'" }, + { name = "tensorpack" }, + { name = "tf-keras", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-keras", version = "2.20.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tf-slim" }, +] +wandb = [ + { name = "wandb" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "nbformat" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +gui-dev = [ + { name = "pytest-qt" }, +] + +[package.metadata] +requires-dist = [ + { name = "albumentations", specifier = "<=1.4.3" }, + { name = "black", marker = "extra == 'dev-docs'", specifier = ">=24" }, + { name = "dlclibrary", specifier = ">=0.0.12" }, + { name = "einops" }, + { name = "filelock", specifier = ">=3.12,<3.16" }, + { name = "filterpy", specifier = ">=1.4.4" }, + { name = "fmpose3d", marker = "extra == 'fmpose3d'", specifier = ">=0.0.8" }, + { name = "h5py", marker = "sys_platform == 'darwin'", specifier = ">=3.15.1" }, + { name = "huggingface-hub", specifier = ">=0.23" }, + { name = "huggingface-hub", marker = "extra == 'modelzoo'" }, + { name = "imageio-ffmpeg" }, + { name = "imgaug", specifier = ">=0.4" }, + { name = "jupyter-book", marker = "extra == 'docs'", specifier = "==1.0.4.post1" }, + { name = "matplotlib", specifier = ">=3.3,!=3.7,!=3.7.1,<3.9" }, + { name = "mike", marker = "extra == 'dev-docs'", specifier = ">=2.1" }, + { name = "mkdocs", marker = "extra == 'dev-docs'", specifier = ">=1.6" }, + { name = "mkdocs-api-autonav", marker = "extra == 'dev-docs'", specifier = ">=0.1" }, + { name = "mkdocs-autorefs", marker = "extra == 'dev-docs'", specifier = ">=1.2" }, + { name = "mkdocs-jupyter", marker = "extra == 'dev-docs'", specifier = ">=0.25" }, + { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'dev-docs'", specifier = ">=9.5" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev-docs'", specifier = ">=0.27" }, + { name = "napari-deeplabcut", marker = "extra == 'gui'", specifier = ">=0.3.1" }, + { name = "networkx", specifier = ">=2.6" }, + { name = "numba", specifier = ">=0.54" }, + { name = "numpy", specifier = ">=1.18.5,<2" }, + { name = "openvino-dev", marker = "extra == 'openvino'", specifier = "==2022.1" }, + { name = "packaging", specifier = ">=26" }, + { name = "pandas", extras = ["hdf5", "performance"], specifier = ">=2.2,<3" }, + { name = "pillow", specifier = ">=7.1" }, + { name = "protobuf", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-cu11'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-cu12'", specifier = "<7" }, + { name = "protobuf", marker = "extra == 'tf-latest'", specifier = "<7" }, + { name = "pycocotools" }, + { name = "pydantic", specifier = ">=2,<3" }, + { name = "pyside6", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'gui'", specifier = "<6.10" }, + { name = "pyside6", marker = "(platform_machine != 'x86_64' and extra == 'gui') or (sys_platform != 'linux' and extra == 'gui')" }, + { name = "pyyaml" }, + { name = "qdarkstyle", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'gui'", specifier = "==3.1" }, + { name = "qdarkstyle", marker = "(platform_machine != 'x86_64' and extra == 'gui') or (sys_platform != 'linux' and extra == 'gui')", specifier = ">=3.1" }, + { name = "ruamel-yaml", specifier = ">=0.15" }, + { name = "scikit-image", specifier = ">=0.17" }, + { name = "scikit-learn", specifier = ">=1" }, + { name = "scipy", specifier = ">=1.9" }, + { name = "sphinxcontrib-mermaid", marker = "extra == 'docs'" }, + { name = "statsmodels", specifier = ">=0.11" }, + { name = "tables", specifier = ">3.8" }, + { name = "tensorflow", marker = "python_full_version >= '3.12' and extra == 'tf'", specifier = ">=2.16.1,<2.18" }, + { name = "tensorflow", marker = "python_full_version < '3.12' and extra == 'tf'", specifier = ">=2.12,<2.16" }, + { name = "tensorflow", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=2.15,<2.18" }, + { name = "tensorflow", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=2.12,<2.15" }, + { name = "tensorflow", marker = "extra == 'tf-cu11'", specifier = "==2.14" }, + { name = "tensorflow", marker = "extra == 'tf-cu12'", specifier = "==2.18" }, + { name = "tensorflow", marker = "extra == 'tf-latest'", specifier = ">=2.18" }, + { name = "tensorflow-io-gcs-filesystem", marker = "python_full_version < '3.12' and sys_platform == 'win32' and extra == 'tf'", specifier = "==0.31" }, + { name = "tensorflow-io-gcs-filesystem", marker = "sys_platform == 'win32' and extra == 'tf-cu11'", specifier = "==0.31" }, + { name = "tensorflow-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=1.2" }, + { name = "tensorflow-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'tf'", specifier = ">=1.2" }, + { name = "tensorflow-metal", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'tf'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-cu11'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-cu12'", specifier = "==1.2" }, + { name = "tensorflow-metal", marker = "sys_platform == 'darwin' and extra == 'tf-latest'", specifier = ">=1.2" }, + { name = "tensorpack", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=0.11" }, + { name = "tensorpack", marker = "extra == 'tf'", specifier = ">=0.11" }, + { name = "tensorpack", marker = "extra == 'tf-cu11'", specifier = "==0.11" }, + { name = "tensorpack", marker = "extra == 'tf-cu12'", specifier = "==0.11" }, + { name = "tensorpack", marker = "extra == 'tf-latest'", specifier = ">=0.11" }, + { name = "tf-keras", marker = "python_full_version >= '3.12' and extra == 'tf'", specifier = ">=2.15,<2.18" }, + { name = "tf-keras", marker = "python_full_version < '3.12' and extra == 'tf'", specifier = "<2.15" }, + { name = "tf-keras", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'" }, + { name = "tf-keras", marker = "extra == 'tf-cu11'", specifier = "==2.14.1" }, + { name = "tf-keras", marker = "extra == 'tf-cu12'", specifier = "==2.18" }, + { name = "tf-keras", marker = "extra == 'tf-latest'" }, + { name = "tf-slim", marker = "sys_platform == 'darwin' and extra == 'apple-mchips'", specifier = ">=1.1" }, + { name = "tf-slim", marker = "extra == 'tf'", specifier = ">=1.1" }, + { name = "tf-slim", marker = "extra == 'tf-cu11'", specifier = "==1.1" }, + { name = "tf-slim", marker = "extra == 'tf-cu12'", specifier = "==1.1" }, + { name = "tf-slim", marker = "extra == 'tf-latest'", specifier = ">=1.1" }, + { name = "timm" }, + { name = "torch", specifier = ">=2" }, + { name = "torch", marker = "extra == 'tf-cu11'", specifier = "<2.1" }, + { name = "torch", marker = "extra == 'tf-cu12'", specifier = "<2.11" }, + { name = "torchvision" }, + { name = "torchvision", marker = "extra == 'tf-cu11'", specifier = "<0.16" }, + { name = "torchvision", marker = "extra == 'tf-cu12'", specifier = "<0.26" }, + { name = "tqdm" }, + { name = "wandb", marker = "extra == 'wandb'" }, +] +provides-extras = ["gui", "openvino", "docs", "dev-docs", "fmpose3d", "tf", "tf-cu11", "tf-cu12", "tf-latest", "apple-mchips", "modelzoo", "wandb"] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage" }, + { name = "nbformat", specifier = ">5" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +gui-dev = [{ name = "pytest-qt" }] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dlclibrary" +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/bf/14c4a73bfb090642a2f8353ff38a623024ee39f7efb4966962ed2a194c3a/dlclibrary-0.0.12.tar.gz", hash = "sha256:9c1dcb98edcba03f33c31e0c0f9d18ce1c349bef65f8d3cd5bde49d5e76171db", size = 13187, upload-time = "2026-05-19T08:04:33.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/57/c0ebe1191a2a445e0a671ce9255b149747ff6285aa6610e355b2e4e72cfe/dlclibrary-0.0.12-py3-none-any.whl", hash = "sha256:be8e22d334fd37cfde6e68936b73ee45a2f0f55baedc83832d773e4f2aedb38b", size = 17488, upload-time = "2026-05-19T08:04:32.456Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/dd/49e06f09b6645156550fb9aee9cc1e59aba7efbc972d665a1bd6ae0435d4/filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", size = 18007, upload-time = "2024-06-22T15:59:14.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f0/48285f0262fe47103a4a45972ed2f9b93e4c80b8fd609fa98da78b2a5706/filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7", size = 16159, upload-time = "2024-06-22T15:59:12.695Z" }, +] + +[[package]] +name = "filterpy" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/1d/ac8914360460fafa1990890259b7fa5ef7ba4cd59014e782e4ab3ab144d8/filterpy-1.4.5.zip", hash = "sha256:4f2a4d39e4ea601b9ab42b2db08b5918a9538c168cff1c6895ae26646f3d73b1", size = 177985, upload-time = "2018-10-10T22:38:24.63Z" } + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + +[[package]] +name = "fmpose3d" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "filterpy" }, + { name = "huggingface-hub" }, + { name = "numba" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "timm" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" } }, + { name = "torchvision", version = "0.28.0", source = { registry = "https://pypi.org/simple" } }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/88/df6b75e3267f859c542244a6eca5edba7ecda4660bcddc6680aa2c5d74b0/fmpose3d-0.0.9.tar.gz", hash = "sha256:89e0137ed3525d2d8b6a4a6276a810643c5afd5556b32f3899eac909287d85bd", size = 114115, upload-time = "2026-04-10T12:14:09.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/1d/fb7a16fafcf437a590462b691372a99e64219e484ebb0e1fa3f849b5eeb9/fmpose3d-0.0.9-py3-none-any.whl", hash = "sha256:9d3a0aea8fdb54234c2dd9ec8d9df5dad3c16350eb7b9aba09f22345165e6d24", size = 131801, upload-time = "2026-04-10T12:14:08.562Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "freetype-py" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/9c/61ba17f846b922c2d6d101cc886b0e8fb597c109cedfcb39b8c5d2304b54/freetype-py-2.5.1.zip", hash = "sha256:cfe2686a174d0dd3d71a9d8ee9bf6a2c23f5872385cf8ce9f24af83d076e2fbd", size = 851738, upload-time = "2024-08-29T18:32:26.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/a8/258dd138ebe60c79cd8cfaa6d021599208a33f0175a5e29b01f60c9ab2c7/freetype_py-2.5.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d01ded2557694f06aa0413f3400c0c0b2b5ebcaabeef7aaf3d756be44f51e90b", size = 1747885, upload-time = "2024-08-29T18:32:17.604Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/280ad06dc944e40789b0a641492321a2792db82edda485369cbc59d14366/freetype_py-2.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2f6b3d68496797da23204b3b9c4e77e67559c80390fc0dc8b3f454ae1cd819", size = 1051055, upload-time = "2024-08-29T18:32:19.153Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/853cad240ec63e21a37a512ee19c896b655ce1772d803a3dd80fccfe63fe/freetype_py-2.5.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:289b443547e03a4f85302e3ac91376838e0d11636050166662a4f75e3087ed0b", size = 1043856, upload-time = "2024-08-29T18:32:20.565Z" }, + { url = "https://files.pythonhosted.org/packages/93/6f/fcc1789e42b8c6617c3112196d68e87bfe7d957d80812d3c24d639782dcb/freetype_py-2.5.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cd3bfdbb7e1a84818cfbc8025fca3096f4f2afcd5d4641184bf0a3a2e6f97bbf", size = 1108180, upload-time = "2024-08-29T18:32:21.871Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1b/161d3a6244b8a820aef188e4397a750d4a8196316809576d015f26594296/freetype_py-2.5.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3c1aefc4f0d5b7425f014daccc5fdc7c6f914fb7d6a695cc684f1c09cd8c1660", size = 1106792, upload-time = "2024-08-29T18:32:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "gast" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyasn1-modules", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests-oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/b4/ef2170c5f6aa5bc2461bab959a84e56d2819ce26662b50038d2d0602223e/google-auth-oauthlib-1.0.0.tar.gz", hash = "sha256:e375064964820b47221a7e1b7ee1fd77051b6323c3f9e3e19785f78ab67ecfc5", size = 20530, upload-time = "2023-02-07T20:53:20.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/07/8d9a8186e6768b55dfffeb57c719bc03770cf8a970a074616ae6f9e26a57/google_auth_oauthlib-1.0.0-py2.py3-none-any.whl", hash = "sha256:95880ca704928c300f48194d1770cf5b1462835b6e49db61445a520f793fd5fb", size = 18926, upload-time = "2023-02-07T20:53:18.837Z" }, +] + +[[package]] +name = "google-pasta" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ae/1db979ff6ae7958d80b288f63d5f6c30df96682700ea9fc340ce994d94a1/greenlet-3.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", size = 619894, upload-time = "2026-04-27T13:02:35.13Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, + { url = "https://files.pythonhosted.org/packages/ce/94/b0590e3d1978f02419f30502341c40d72f77eb0a2198119fe27df47714ee/greenlet-3.5.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", size = 415681, upload-time = "2026-04-27T13:05:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h5py" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform == 'darwin' or extra == 'extra-10-deeplabcut-tf' or extra == 'extra-10-deeplabcut-tf-cu11' or extra == 'extra-10-deeplabcut-tf-cu12' or extra == 'extra-10-deeplabcut-tf-latest'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/6b/231413e58a787a89b316bb0d1777da3c62257e4797e09afd8d17ad3549dc/h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9", size = 3724137, upload-time = "2026-03-06T13:47:35.242Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/557ce3aad0fe8471fb5279bab0fc56ea473858a022c4ce8a0b8f303d64e9/h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3", size = 3090112, upload-time = "2026-03-06T13:47:37.634Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/e15b3d0dc8a18e56409a839e6468d6fb589bc5207c917399c2e0706eeb44/h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d", size = 4844847, upload-time = "2026-03-06T13:47:39.811Z" }, + { url = "https://files.pythonhosted.org/packages/cb/92/a8851d936547efe30cc0ce5245feac01f3ec6171f7899bc3f775c72030b3/h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d", size = 5065352, upload-time = "2026-03-06T13:47:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ae/f2adc5d0ca9626db3277a3d87516e124cbc5d0eea0bd79bc085702d04f2c/h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f", size = 4839173, upload-time = "2026-03-06T13:47:43.586Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e0c8c69da1d8838da023a50cd3080eae5d475691f7636b35eff20bb6ef20/h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c", size = 5076216, upload-time = "2026-03-06T13:47:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/d88fd6718832133c885004c61ceeeb24dbd6397ef877dbed6b3a64d6a286/h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d", size = 3183639, upload-time = "2026-03-06T13:47:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ba/95/a825894f3e45cbac7554c4e97314ce886b233a20033787eda755ca8fecc7/h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af", size = 3721663, upload-time = "2026-03-06T13:47:49.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/38ff88b347c3e346cda1d3fc1b65a7aa75d40632228d8b8a5d7b58508c24/h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447", size = 3087630, upload-time = "2026-03-06T13:47:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, + { url = "https://files.pythonhosted.org/packages/f7/20/e6c0ff62ca2ad1a396a34f4380bafccaaf8791ff8fccf3d995a1fc12d417/h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999", size = 3191673, upload-time = "2026-03-06T13:48:00.626Z" }, + { url = "https://files.pythonhosted.org/packages/f2/48/239cbe352ac4f2b8243a8e620fa1a2034635f633731493a7ff1ed71e8658/h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859", size = 2673834, upload-time = "2026-03-06T13:48:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9e/6142ebfda0cb6e9349c091eae73c2e01a770b7659255248d637bec54a88b/h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9", size = 3671808, upload-time = "2026-03-06T13:48:19.737Z" }, + { url = "https://files.pythonhosted.org/packages/b0/65/5e088a45d0f43cd814bc5bec521c051d42005a472e804b1a36c48dada09b/h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb", size = 3045837, upload-time = "2026-03-06T13:48:21.854Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/6172269e18cc5a484e2913ced33339aad588e02ba407fafd00d369e22ef3/h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524", size = 5193860, upload-time = "2026-03-06T13:48:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/ef2b6fe2903e377cbe870c3b2800d62552f1e3dbe81ce49e1923c53d1c5c/h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402", size = 5400417, upload-time = "2026-03-06T13:48:25.728Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/5b62d760039eed64348c98129d17061fdfc7839fc9c04eaaad6dee1004e4/h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7", size = 5185214, upload-time = "2026-03-06T13:48:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/28/c4/532123bcd9080e250696779c927f2cb906c8bf3447df98f5ceb8dcded539/h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff", size = 5414598, upload-time = "2026-03-06T13:48:29.49Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/a27997f84341fc0dfcdd1fe4179b6ba6c32a7aa880fdb8c514d4dad6fba3/h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad", size = 3175509, upload-time = "2026-03-06T13:48:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/bb8647521d4fd770c30a76cfc6cb6a2f5495868904054e92f2394c5a78ff/h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4", size = 2647362, upload-time = "2026-03-06T13:48:33.411Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/7fcd9b4c9eed82e91fb15568992561019ae7a829d1f696b2c844355d95dd/h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65", size = 3678608, upload-time = "2026-03-06T13:48:35.183Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210", size = 3054773, upload-time = "2026-03-06T13:48:37.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/4964bc0e91e86340c2bbda83420225b2f770dcf1eb8a39464871ad769436/h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965", size = 5198886, upload-time = "2026-03-06T13:48:38.879Z" }, + { url = "https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd", size = 5404883, upload-time = "2026-03-06T13:48:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/58f34cb74af46d39f4cd18ea20909a8514960c5a3e5b92fd06a28161e0a8/h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c", size = 5192039, upload-time = "2026-03-06T13:48:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/934a39c24ce2e2db017268c08da0537c20fa0be7e1549be3e977313fc8f5/h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc", size = 5421526, upload-time = "2026-03-06T13:48:44.838Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab", size = 3183263, upload-time = "2026-03-06T13:48:47.117Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/a6faef5ed632cae0c65ac6b214a6614a0b510c3183532c521bdb0055e117/h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63", size = 2663450, upload-time = "2026-03-06T13:48:48.707Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/0c8bb8aedb62c772cf7c1d427c7d1951477e8c2835f872bc0a13d1f85f86/h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491", size = 3760693, upload-time = "2026-03-06T13:48:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1f/fcc5977d32d6387c5c9a694afee716a5e20658ac08b3ff24fdec79fb05f2/h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618", size = 3181305, upload-time = "2026-03-06T13:48:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/af87f64b9f986889884243643621ebbd4ac72472ba8ec8cec891ac8e2ca1/h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242", size = 5074061, upload-time = "2026-03-06T13:48:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d0/146f5eaff3dc246a9c7f6e5e4f42bd45cc613bce16693bcd4d1f7c958bf5/h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16", size = 5279216, upload-time = "2026-03-06T13:48:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/12a13424f1e604fc7df9497b73c0356fb78c2fb206abd7465ce47226e8fd/h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7", size = 5070068, upload-time = "2026-03-06T13:48:59.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/bbe98f813722b4873818a8db3e15aa3e625b59278566905ac439725e8070/h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725", size = 5300253, upload-time = "2026-03-06T13:49:02.033Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/87e6705b4d6890e7cecdf876e2a7d3e40654a2ae37482d79a6f1b87f7b92/h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e", size = 3381671, upload-time = "2026-03-06T13:49:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/91/9fad90cfc5f9b2489c7c26ad897157bce82f0e9534a986a221b99760b23b/h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1", size = 2740706, upload-time = "2026-03-06T13:49:06.347Z" }, +] + +[[package]] +name = "heapdict" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/9b/d8963ae7e388270b695f3b556b6dc9adb70ae9618fba09aa1e7b1886652d/HeapDict-1.0.1.tar.gz", hash = "sha256:8495f57b3e03d8e46d5f1b2cc62ca881aca392fd5cc048dc0aa2e1a6d23ecdb6", size = 4274, upload-time = "2019-09-09T18:57:02.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9d/cd4777dbcf3bef9d9627e0fe4bc43d2e294b1baeb01d0422399d5e9de319/HeapDict-1.0.1-py3-none-any.whl", hash = "sha256:6065f90933ab1bb7e50db403b90cab653c853690c5992e69294c2de2b253fc92", size = 3917, upload-time = "2019-09-09T18:57:00.821Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "hsluv" +version = "5.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/81/af16607fa045724e515579d312577261b436f36f419e7c677e7e88fcc943/hsluv-5.0.4.tar.gz", hash = "sha256:2281f946427a882010042844a38c7bbe9e0d0aaf9d46babe46366ed6f169b72e", size = 543090, upload-time = "2023-09-11T21:46:52.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/36/5bddefea3d7adf22a64f9aa9701492f8a9fe6948223f5cf2602c22ec9be7/hsluv-5.0.4-py2.py3-none-any.whl", hash = "sha256:0138bd10038e2ee1b13eecae9a7d49d4ec8c320b1d7eb4f860832c792e3e4567", size = 5252, upload-time = "2023-09-11T21:46:50.407Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "imgaug" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "shapely" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/7d/820295b8fdaf06dce9688ef2fdeb5a317896d3276db7723e5a94e85e1253/imgaug-0.4.0.tar.gz", hash = "sha256:46bab63ed38f8980630ff721a09ca2281b7dbd4d8c11258818b6ebcc69ea46c7", size = 937254, upload-time = "2020-02-05T20:54:24.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/b1/af3142c4a85cba6da9f4ebb5ff4e21e2616309552caca5e8acefe9840622/imgaug-0.4.0-py2.py3-none-any.whl", hash = "sha256:ce61e65b4eb7405fc62c1b0a79d2fa92fd47f763aaecb65152d29243592111f9", size = 948018, upload-time = "2020-02-05T20:54:22.293Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "in-n-out" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/08/07edfac98a38ab0208557524cbdd94a296f565b0558417ccb2c03d14a6ea/in_n_out-0.2.1.tar.gz", hash = "sha256:43cde2b7de981d41a6d70618a2b7bd989481095922a53ead4dc75f2bbd5dffea", size = 26026, upload-time = "2024-04-22T18:56:50.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/06/711a4d105ad3d01d3ef351a1039bb5cc517a57dbf377d7da9a0808e34c77/in_n_out-0.2.1-py3-none-any.whl", hash = "sha256:343e81edb27cf41ec946134a92964f408465abdf6a065c6c55fe96f53bc3c8b7", size = 19951, upload-time = "2024-04-22T18:56:48.572Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "decorator", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jedi", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pexpect", marker = "(python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "stack-data", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "traitlets", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, +] + +[[package]] +name = "ipython" +version = "9.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.11' and sys_platform == 'win32') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "decorator", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jedi", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pexpect", marker = "(python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'emscripten' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'win32' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "stack-data", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "traitlets", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-book" +version = "1.0.4.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "linkify-it-py" }, + { name = "myst-nb" }, + { name = "myst-parser" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "sphinx-book-theme", version = "1.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-book-theme", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-comments" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx-external-toc" }, + { name = "sphinx-jupyterbook-latex" }, + { name = "sphinx-multitoc-numbering" }, + { name = "sphinx-thebe" }, + { name = "sphinx-togglebutton" }, + { name = "sphinxcontrib-bibtex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/ee/5d10ce5b161764ad44219853f386e98b535cb3879bcb0d7376961a1e3897/jupyter_book-1.0.4.post1.tar.gz", hash = "sha256:2fe92c49ff74840edc0a86bb034eafdd0f645fca6e48266be367ce4d808b9601", size = 67412, upload-time = "2025-02-28T14:55:48.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/86/d45756beaeb4b9b06125599b429451f8640b5db6f019d606f33c85743fd4/jupyter_book-1.0.4.post1-py3-none-any.whl", hash = "sha256:3a27a6b2581f1894ffe8f347d1a3432f06fc616997547919c42cd41c54db625d", size = 45005, upload-time = "2025-02-28T14:55:46.561Z" }, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9", size = 32048, upload-time = "2024-11-15T16:03:55.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6", size = 33907, upload-time = "2024-11-15T16:03:54.021Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupytext" +version = "1.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/2d/15624c3d9440d85a280ff13d2d23afd989802f25470ac59932f4fef6f0c6/jupytext-1.19.3.tar.gz", hash = "sha256:713c3ed4441afe0f31474d28ea2e6b61a268c04c40fd78e5ccfd7f7ac9e9f766", size = 4305350, upload-time = "2026-05-17T09:09:29.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/ec/d9be3bd1db141e76b2f525c265f70e66edd30a51a3307d8edf0ef1909c54/jupytext-1.19.3-py3-none-any.whl", hash = "sha256:acf75492f80895ad8e664fd8db1708b617008dd0e71c341a1abc3d0d07310ed0", size = 170579, upload-time = "2026-05-17T09:09:27.478Z" }, +] + +[[package]] +name = "keras" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/85/d52a86eb5ae700e1f8694157019249eb33350ae9e477cd03ecdb50939d22/keras-2.14.0.tar.gz", hash = "sha256:22788bdbc86d9988794fe9703bb5205141da797c4faeeb59497c58c3d94d34ed", size = 1251354, upload-time = "2023-09-11T17:21:04.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/58/34d4d8f1aa11120c2d36d7ad27d0526164b1a8ae45990a2fede31d0e59bf/keras-2.14.0-py3-none-any.whl", hash = "sha256:d7429d1d2131cc7eb1f2ea2ec330227c7d9d38dab3dfdf2e78defee4ecc43fcd", size = 1709236, upload-time = "2023-09-11T17:21:02.164Z" }, +] + +[[package]] +name = "keras" +version = "3.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.11'" }, + { name = "h5py", marker = "python_full_version < '3.11'" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "namex", marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "optree", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "rich", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/73/19e057f7a2a6d641246bacca21e0bbcb2be341afca98ea461a0f2a9ab92d/keras-3.12.2.tar.gz", hash = "sha256:e19c7c7f8f2a81e44d4f203e567731a15a270d8ef351060982b45a1fafdf3fce", size = 1129833, upload-time = "2026-05-07T21:48:18.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/ba/1f2daa7940d7c5c65efc85370453e9e67ace0d06b4a7346b53f0e7355453/keras-3.12.2-py3-none-any.whl", hash = "sha256:0433310d7d626d5cbbc58e98223b3a77ce7d7d4398bf7e169d4e8bdcf9ce0296", size = 1476474, upload-time = "2026-05-07T21:48:16.057Z" }, +] + +[[package]] +name = "keras" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "h5py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "namex", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "optree", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, + { name = "rich", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.11.*' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/e7/97a7664581b73e4f9ff1d3a767a493b6ac5d3e0ed1926bd2b6b2c8bbccd7/keras-3.14.1.tar.gz", hash = "sha256:ef479173102ad29db89b53c232efdc3fb5ad57c28bc27ead59f3e78a1eecd05b", size = 1263647, upload-time = "2026-05-07T21:43:35.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/03/184267c1d09783dd070f1ddfd0d4beb7503139dfc7bd75b422867cf282fd/keras-3.14.1-py3-none-any.whl", hash = "sha256:ebd2c14d2af3c9de18083604d408483996407fc7d2f9ebd1d565961f96608c29", size = 1628606, upload-time = "2026-05-07T21:43:32.737Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "latexcodec" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/dd/4270b2c5e2ee49316c3859e62293bd2ea8e382339d63ab7bbe9f39c0ec3b/latexcodec-3.0.1.tar.gz", hash = "sha256:e78a6911cd72f9dec35031c6ec23584de6842bfbc4610a9678868d14cdfb0357", size = 31222, upload-time = "2025-06-17T18:47:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "libclang" +version = "18.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612, upload-time = "2024-03-17T16:04:37.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045, upload-time = "2024-06-30T17:40:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641, upload-time = "2024-03-18T15:52:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207, upload-time = "2024-03-17T15:00:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943, upload-time = "2024-03-17T16:03:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972, upload-time = "2024-03-17T16:12:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606, upload-time = "2024-03-17T16:17:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494, upload-time = "2024-03-17T16:14:20.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083, upload-time = "2024-03-17T16:42:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112, upload-time = "2024-03-17T16:42:59.565Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "lit" +version = "18.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b4/d7e210971494db7b9a9ac48ff37dfa59a8b14c773f9cf47e6bda58411c0d/lit-18.1.8.tar.gz", hash = "sha256:47c174a186941ae830f04ded76a3444600be67d5e5fb8282c3783fba671c4edb", size = 161127, upload-time = "2024-06-25T14:33:14.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/b36f150fa7c5bcc96a31a4d19a20fddbd1d965b6f02510b57a3bb8d4b930/lit-18.1.8-py3-none-any.whl", hash = "sha256:a873ff7acd76e746368da32eb7355625e2e55a2baaab884c9cc130f2ee0300f7", size = 96365, upload-time = "2024-06-25T14:33:12.101Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "magicgui" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "psygnal" }, + { name = "qtpy" }, + { name = "superqt", extra = ["iconify"] }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/9c/0d918ee0a9b31f16e9b76c1b86b81b5d4b7bc491354c76030b87790f0cab/magicgui-0.10.2.tar.gz", hash = "sha256:ae7a4cbb7ef2028b827b1877cf0b06743d756074fe6ef849391d62448ab7b65d", size = 20946219, upload-time = "2026-04-10T14:45:28.927Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/bb/5ba264d3ccc13294e74c48c3ef0c2e96b8b13765562628515654012cc47e/magicgui-0.10.2-py3-none-any.whl", hash = "sha256:0f304d4da1a4309ad15d38cb809ef73269cea73a3195ac944f38d7c56d8e0fd5", size = 128254, upload-time = "2026-04-10T14:45:27.173Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.8.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/4f/8487737a74d8be4ab5fbe6019b0fae305c1604cf7209500969b879b5f462/matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea", size = 35934425, upload-time = "2024-04-04T01:47:18.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/c0/1f88491656d21a2fecd90fbfae999b2f87bc44d439ef301ec8e0e4a937a0/matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014", size = 7603557, upload-time = "2024-04-04T01:47:46.363Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/aa059a4fb8154d5875a5ddd33f8d0a42d77c0225fe4325e9b9358f39b0bf/matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106", size = 7497421, upload-time = "2024-04-04T01:47:54.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/67/ded5217d42de1532193cd87db925c67997d23c68b20c3eaa9e4c6a0adb67/matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10", size = 11377985, upload-time = "2024-04-04T01:48:04.955Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/061f97211f942101070a46fecd813a6b1bd83590ed7b07c473cabd707fe7/matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0", size = 11608003, upload-time = "2024-04-04T01:48:16.25Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d3/5d0bb1d905e219543fdfd7ab04e9d641a766367c83a5ffbcea60d2b2cf2d/matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef", size = 9535368, upload-time = "2024-04-04T01:48:26.265Z" }, + { url = "https://files.pythonhosted.org/packages/62/5a/a5108ae3db37f35f8a2be8a57d62da327af239214c9661464ce09ee32d7d/matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338", size = 7656037, upload-time = "2024-04-04T01:48:34.761Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/62250ea25780d4b59c2c6044ec161235c47cc05a18d0ec0a05657de75b7d/matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661", size = 7606117, upload-time = "2024-04-04T01:48:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/60/12d4f27b859a74359306662da69c2d08826a2b05cfe7f96e66b490f41573/matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c", size = 7500108, upload-time = "2024-04-04T01:48:50.21Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/9e4f7f34dccf2d2768504410410db8d551c940457a2bec658dc4fa3b5aa2/matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa", size = 11382998, upload-time = "2024-04-04T01:49:01.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/e363612ac1a514abfb5505aa209dd5b724b3232a6de98710d7759559706a/matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71", size = 11613309, upload-time = "2024-04-04T01:49:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/32/4c/63164901acadb3ada55c5e0fd6b7f29c9033d7e131302884cd735611b77a/matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b", size = 9546019, upload-time = "2024-04-04T01:49:23.752Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d5/6227732ecab9165586966ccb54301e3164f61b470c954c4cf6940654fbe1/matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae", size = 7658174, upload-time = "2024-04-04T01:49:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/91/eb/65f3bd78ce757dadd455c220273349428384b162485cd8aa380b61a867ed/matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616", size = 7604083, upload-time = "2024-04-04T01:49:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/da/2b/2bb6073ca8d336da07ace7d98bf7bb9da8233f55876bb3db6a5ee924f3e9/matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732", size = 7496013, upload-time = "2024-04-04T01:49:48.174Z" }, + { url = "https://files.pythonhosted.org/packages/61/cd/976d3a9c10328da1d2fe183f7c92c45f1e125536226a6eb3a820c4753cd1/matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb", size = 11376749, upload-time = "2024-04-04T01:49:58.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ba/412149958e951876096198609b958b90a8a2c9bc07a96eeeaa9e2c480f30/matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30", size = 11600837, upload-time = "2024-04-04T01:50:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4f/e5b56ca109d8ab6bae37f519f15b891fc18809ddb8bc1aa26e0bfca83e25/matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25", size = 9538883, upload-time = "2024-04-04T01:50:19.268Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ca/e7bd1876a341ed8c456095962a582696cac1691cb6e55bd5ead15a755c5d/matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a", size = 7659712, upload-time = "2024-04-04T01:50:26.938Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mike" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "verspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-api-autonav" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "mkdocstrings-python" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b0/20960ee733a419a349877d09712d02e8ec2bda031672e5f0d0a15fc020b3/mkdocs_api_autonav-0.4.0.tar.gz", hash = "sha256:3527b0e5cf1b682bd374a3ce699ac12d6288f5fcaf93877f34a6b14c79740637", size = 17987, upload-time = "2025-09-09T12:42:02.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/37/e1413281aec69994a0ecb8baaff523b7b7da3119ae7d495b7dc659e630b0/mkdocs_api_autonav-0.4.0-py3-none-any.whl", hash = "sha256:87474e7919664fca75648a05e79de238dd5b39a0f711910d3638626b016acfe3", size = 13130, upload-time = "2025-09-09T12:42:00.731Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-jupyter" +version = "0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupytext" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "nbconvert" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/aa/f8d15409a9a3112486994a80d5a975694c7d145c4f8b5b484aeb383420ef/mkdocs_jupyter-0.26.3.tar.gz", hash = "sha256:e1e8bd48a1b96542e84e3028e3066112bac7b94d95ab69f8b91305c84003ca26", size = 1628353, upload-time = "2026-04-17T18:56:31.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl", hash = "sha256:cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82", size = 1459740, upload-time = "2026-04-17T18:56:30.031Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[package.optional-dependencies] +imaging = [ + { name = "cairosvg" }, + { name = "pillow" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/b4/5fed370d8ebd96e4e399460a7146ae989263f16588b05a6facd6dbd51e60/mkdocstrings_python-2.0.4.tar.gz", hash = "sha256:58c73c5d358e64e9b1673447663f4a2f8a8941e392e225fc0a0c893758cc452f", size = 199219, upload-time = "2026-06-05T08:13:01.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e3/00ec594aef5f55522e6d373bc2ac53e53a8f5e9ae32f2d6854b0de4270f3/mkdocstrings_python-2.0.4-py3-none-any.whl", hash = "sha256:fd87c173e1e719a85997b6d4f852cdc55f36710e0ed08da3a7bd9abe79c9db00", size = 104790, upload-time = "2026-06-05T08:13:00.393Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/47/09ca9556bf99cfe7ddf129a3423642bd482a27a717bf115090493fa42429/ml_dtypes-0.2.0.tar.gz", hash = "sha256:6488eb642acaaf08d8020f6de0a38acee7ac324c1e6e92ee0c0fea42422cb797", size = 698948, upload-time = "2023-06-06T15:14:43.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/9f/3c133f83f3e5a7959345585e9ac715ef8bf6e8987551f240032e1b0d3ce6/ml_dtypes-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df6a76e1c8adf484feb138ed323f9f40a7b6c21788f120f7c78bec20ac37ee81", size = 1154492, upload-time = "2023-06-06T15:14:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/19/05/7a6480a69f8555a047a56ae6af9490bcdc5e432658208f3404d8e8442d02/ml_dtypes-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc29a0524ef5e23a7fbb8d881bdecabeb3fc1d19d9db61785d077a86cb94fab2", size = 1012633, upload-time = "2023-06-06T15:14:14.055Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1d/d5cf76e5e40f69dbd273036e3172ae4a614577cb141673427b80cac948df/ml_dtypes-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08c391c2794f2aad358e6f4c70785a9a7b1df980ef4c232b3ccd4f6fe39f719", size = 1017764, upload-time = "2023-06-06T15:14:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/51/c430b4f5f4a6df00aa41c1ee195e179489565e61cfad559506ca7442ce67/ml_dtypes-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:75015818a7fccf99a5e8ed18720cb430f3e71a8838388840f4cdf225c036c983", size = 938593, upload-time = "2023-06-06T15:14:17.473Z" }, + { url = "https://files.pythonhosted.org/packages/15/da/43bee505963da0c730ee50e951c604bfdb90d4cccc9c0044c946b10e68a7/ml_dtypes-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e70047ec2c83eaee01afdfdabee2c5b0c133804d90d0f7db4dd903360fcc537c", size = 1154491, upload-time = "2023-06-06T15:14:19.199Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/01570d615d16f504be091b914a6ae9a29e80d09b572ebebc32ecb1dfb22d/ml_dtypes-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36d28b8861a8931695e5a31176cad5ae85f6504906650dea5598fbec06c94606", size = 1012631, upload-time = "2023-06-06T15:14:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/d57c2d22e4801edeb7f3e7939214c0ea8a28c6e16f85208c2df2145e0213/ml_dtypes-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e85ba8e24cf48d456e564688e981cf379d4c8e644db0a2f719b78de281bac2ca", size = 1017764, upload-time = "2023-06-06T15:14:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/08/89/c727fde1a3d12586e0b8c01abf53754707d76beaa9987640e70807d4545f/ml_dtypes-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:832a019a1b6db5c4422032ca9940a990fa104eee420f643713241b3a518977fa", size = 938744, upload-time = "2023-06-06T15:14:25.77Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version == '3.12.*' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9e/76b84f77c7afee3b116dc8407903a2d5004ba3059a8f3dcdcfa6ebf33fff/ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5", size = 397975, upload-time = "2024-09-13T19:06:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/03/7b/32650e1b2a2713a5923a0af2a8503d0d4a8fc99d1e1e0a1c40e996634460/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24", size = 2182570, upload-time = "2024-09-13T19:06:46.189Z" }, + { url = "https://files.pythonhosted.org/packages/16/86/a9f7569e7e4f5395f927de38a13b92efa73f809285d04f2923b291783dd2/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354", size = 2160365, upload-time = "2024-09-13T19:06:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/9a3afb437702503514f3934ec8d7904270edf013d28074f3e700e5dfbb0f/ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f", size = 126633, upload-time = "2024-09-13T19:06:50.656Z" }, + { url = "https://files.pythonhosted.org/packages/d1/76/9835c8609c29f2214359e88f29255fc4aad4ea0f613fb48aa8815ceda1b6/ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975", size = 397973, upload-time = "2024-09-13T19:06:51.748Z" }, + { url = "https://files.pythonhosted.org/packages/7e/99/e68c56fac5de973007a10254b6e17a0362393724f40f66d5e4033f4962c2/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9", size = 2185134, upload-time = "2024-09-13T19:06:53.197Z" }, + { url = "https://files.pythonhosted.org/packages/28/bc/6a2344338ea7b61cd7b46fb24ec459360a5a0903b57c55b156c1e46c644a/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752", size = 2163661, upload-time = "2024-09-13T19:06:54.519Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d3/ddfd9878b223b3aa9a930c6100a99afca5cfab7ea703662e00323acb7568/ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6", size = 126727, upload-time = "2024-09-13T19:06:55.897Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077, upload-time = "2024-09-13T19:06:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554, upload-time = "2024-09-13T19:06:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488, upload-time = "2024-09-13T19:07:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462, upload-time = "2024-09-13T19:07:04.916Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "(python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "msgpack-numpy" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/94/61e8aee142733ebfdc400a05bdac6e1763c4514bba3b42743d223f388450/msgpack-numpy-0.4.8.tar.gz", hash = "sha256:c667d3180513422f9c7545be5eec5d296dcbb357e06f72ed39cc683797556e69", size = 10923, upload-time = "2022-06-09T03:43:08.739Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl", hash = "sha256:773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da", size = 6919, upload-time = "2022-06-09T03:43:06.82Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "myst-nb" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "ipykernel" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jupyter-cache" }, + { name = "myst-parser" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/b4/ff1abeea67e8cfe0a8c033389f6d1d8b0bfecfd611befb5cbdeab884fce6/myst_nb-1.4.0.tar.gz", hash = "sha256:c145598de62446a6fd009773dd071a40d3b76106ace780de1abdfc6961f614c2", size = 82285, upload-time = "2026-03-02T21:14:56.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/93/0a378b48488879a1d925b42a804edfc6e0cd0ef854220f2dce738a46e7e9/myst_nb-1.4.0-py3-none-any.whl", hash = "sha256:0e2c86e7d3b82c3aa51383f82d6268f7714f3b772c23a796ab09538a8e68b4e4", size = 82555, upload-time = "2026-03-02T21:14:55.652Z" }, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, +] + +[[package]] +name = "namex" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649, upload-time = "2025-05-26T23:17:38.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905, upload-time = "2025-05-26T23:17:37.695Z" }, +] + +[[package]] +name = "napari" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "app-model" }, + { name = "appdirs" }, + { name = "cachey" }, + { name = "certifi" }, + { name = "dask", extra = ["array"] }, + { name = "imageio" }, + { name = "jsonschema" }, + { name = "lazy-loader" }, + { name = "magicgui" }, + { name = "napari-console" }, + { name = "napari-plugin-engine" }, + { name = "napari-svg" }, + { name = "npe2" }, + { name = "numpy" }, + { name = "numpydoc" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pint", version = "0.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pint", version = "0.25.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pygments" }, + { name = "pyopengl" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyyaml" }, + { name = "qtpy" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, extra = ["data"], marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, extra = ["data"], marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "superqt" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "toolz" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "vispy" }, + { name = "wrapt", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/f1573c137aeccb86f8a57cae2470b28e22f55b8fa9fa5a2c2621ff1457de/napari-0.6.6.tar.gz", hash = "sha256:8e1adfc737c55c2a619689fad8d9e4d115582e56f09096cf771816b0ec75c3a7", size = 3251386, upload-time = "2025-10-16T09:25:58.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/d8/61c29378edc7e75aa5f877b3d1b851ca357cb47b8dd4b6268d07abf7d9d3/napari-0.6.6-py3-none-any.whl", hash = "sha256:c65cc13de1901ec2bf8d11c665e8df9f39e678d98b76cf690d1094e9decaca69", size = 3537483, upload-time = "2025-10-16T09:25:55.679Z" }, +] + +[[package]] +name = "napari-console" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ipython", version = "9.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "qtconsole" }, + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/6e1fcd9aa9ac4746ce2b44050ea8f7192d883f4d3da4e7ff08589ac3ad3b/napari_console-0.1.4.tar.gz", hash = "sha256:e185e4d36d8171ae23ca383dc69c38df76592a984d6c99ad08372d188a1fbb9b", size = 20152, upload-time = "2025-10-15T14:24:18.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/72/2067f28fd0ae87978f3b61e8ec30c1d085bbed03f64eb58e43949d526b3a/napari_console-0.1.4-py3-none-any.whl", hash = "sha256:565df1fa15db579552af9e9d9d3883067c00191be282ad47d80f9b0d50b4e5ad", size = 9786, upload-time = "2025-10-15T14:24:17.677Z" }, +] + +[[package]] +name = "napari-deeplabcut" +version = "0.3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dask-image" }, + { name = "matplotlib" }, + { name = "napari" }, + { name = "natsort" }, + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pandas" }, + { name = "pyside6" }, + { name = "pyyaml" }, + { name = "qtpy" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "shiboken6" }, + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/3e3956a7278c17ba4b263d24a270404cd782b1bcec3de6f860cbdeca057f/napari_deeplabcut-0.3.1.0.tar.gz", hash = "sha256:c4bc3a3643dd984f2045e59d5c06f299c7388fce1832ee7478ee0be7fddc5872", size = 1941735, upload-time = "2026-05-19T10:46:46.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/13/b43e8f1bc5d279178509c6b6929bb8c84ad16d73be5da0afc2452dc8d60a/napari_deeplabcut-0.3.1.0-py3-none-any.whl", hash = "sha256:84ee5c3cf8a2eecb0d8a59ec522d77e690987b77d33862a35b8dd4d9522253ac", size = 1194984, upload-time = "2026-05-19T10:46:44.313Z" }, +] + +[[package]] +name = "napari-plugin-engine" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/72/c653308edaf7f7c84d82e388a1c46bc8f26c385027af58b5bf728f600b47/napari_plugin_engine-0.2.1.tar.gz", hash = "sha256:46829cf02f368c8f2f1aa8b998ec73bcf14a2c1f5c15abd94b82154d7aef510d", size = 55809, upload-time = "2026-02-10T08:31:20.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/bc/2509813cddd0e02736121e21bef54deeec7f0f89af2c6096d753ee1feb09/napari_plugin_engine-0.2.1-py3-none-any.whl", hash = "sha256:de30babe6fd9477816f037207938a2da7faeddc0e9e8663cb29f3d74235b6dc5", size = 33849, upload-time = "2026-02-10T08:31:19.037Z" }, +] + +[[package]] +name = "napari-svg" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "vispy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/90/802f8288d16c1513b908d644779e733461a53b6c1a2c7561f1464c9f1516/napari_svg-0.2.1.tar.gz", hash = "sha256:031f13b34b0948afbdcb11eb00728fe32ef7e4e3aa3905f923001d6871a08ad9", size = 17533, upload-time = "2025-01-14T07:26:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ae/0eeb22806c4157a9199f65a93374e5ff5c4d2cc1411b5d25053bcd9e6b91/napari_svg-0.2.1-py3-none-any.whl", hash = "sha256:9eaa54fbbf9bfd5078b67b7d1edc9eccfd872dab89fd586374909fef4ed89a49", size = 16458, upload-time = "2025-01-14T07:26:29.328Z" }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "ndindex" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/92/4b9d2f4e0f3eabcfc7b02b48261f6e5ad36a3e2c1bbdcc4e3b7b6c768fa6/ndindex-1.10.1.tar.gz", hash = "sha256:0f6113c1f031248f8818cbee1aa92aa3c9472b7701debcce9fddebcd2f610f11", size = 271395, upload-time = "2025-11-19T20:40:08.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/71/aff23bd84111d038efdcdaea4d218b463a0b2129ff49f30613cbc6f535ff/ndindex-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8644c76e74c0fbbdaa54752de30b7c6b98b1e8f6c05f0c6228632a29c862d83f", size = 172022, upload-time = "2025-11-19T20:38:12.429Z" }, + { url = "https://files.pythonhosted.org/packages/99/a6/adcc17b685b24362983b00f965ee5c8607f74e7c68049a20facbd7ceb0b6/ndindex-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9a211ec2198994cb3600cd46adb335a740f27e4d406b40d48ed7b98d2d2a89b", size = 171057, upload-time = "2025-11-19T20:38:13.846Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/b0b1bde7818d2ccd5c288802c1f24b69705e03f3975bc948c005eccab25a/ndindex-1.10.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdb86a4176f2ae23bd4bcd0401ca35d5dad2d1ed0d0dca1ff64480ebe41b75d9", size = 498925, upload-time = "2025-11-19T20:38:17.214Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/55c3800048ef5310de542f188e1aad00e0b1d37713230c0eae980e88c895/ndindex-1.10.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ce3bd0882572269ca09285112cf38ce84baa2aaa5891551af968ca7c18f84bb", size = 495662, upload-time = "2025-11-19T20:38:20.026Z" }, + { url = "https://files.pythonhosted.org/packages/48/a4/0103c3ee3778d7079c3ff7dd879c79362afe3a7e9d3b8dcdaa25b49ca413/ndindex-1.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d6442ecce9b395aade5e9f2431e169e01393953a069f6d2d53a63b6c94d1d06", size = 1471263, upload-time = "2025-11-19T20:38:21.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/5a/eaa38b18757c3d8e7b2438faa5001a02f193b51a68a5558d6066f3c407e6/ndindex-1.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bada24abee6bc6ca438b2e6b68a752fc9b58b67bdcb54008e2bc6330ecb0a777", size = 1522878, upload-time = "2025-11-19T20:38:23.064Z" }, + { url = "https://files.pythonhosted.org/packages/a3/93/a40920c849fa128c9439bc3eb0add814696216dde235497eaa415f14d5e7/ndindex-1.10.1-cp310-cp310-win32.whl", hash = "sha256:bc236d1612714cbd80610cf25a6ef92584ff1402e9d5a5c50e926195716f7d22", size = 149268, upload-time = "2025-11-19T20:38:25.12Z" }, + { url = "https://files.pythonhosted.org/packages/85/d9/baf1655d0b2d36eb46134fddf7dd0ef0093203c9c91d17f8ce01b9060366/ndindex-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:4cea15cff221e76abd12e3e940c26124184735cf421c229307f5db6742e14dd7", size = 157151, upload-time = "2025-11-19T20:38:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d9/c94ab6151c9fdd199c2b560f23e3759a9fb86a7a1275855e0b97291bf05a/ndindex-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e2ad917bcdf8dc5ba1e21f01054c991d26862d4d01c3c203a50e907096d558ac", size = 172128, upload-time = "2025-11-19T20:38:28.977Z" }, + { url = "https://files.pythonhosted.org/packages/3a/34/880c4073750766e44492d51280d025f28e36475394ca3d741b0a4adad4b0/ndindex-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e851990a68937db5f485cd9f3e760c1fd47fa0f2a99f63a5e2cc880908faf3bb", size = 171423, upload-time = "2025-11-19T20:38:30.357Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1e/0342da55dabe4075efc2b2ab91a6a22ed3047c5bd511ef771a7a3f822c90/ndindex-1.10.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27385939f317b55773ea53f6bf9334810cf1d66206034c0a6a6f2a88f2001c3c", size = 519590, upload-time = "2025-11-19T20:38:32.464Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cb/7a02b6f29b15a16cd0002f4591d14493eff8e9236f7ca4c02ee4d4bcefbd/ndindex-1.10.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf3ca16efcdfbb8800aa88fbab1bc6528e6a0504bcb9cf7af4cb9d50e9f5d9", size = 516676, upload-time = "2025-11-19T20:38:34.276Z" }, + { url = "https://files.pythonhosted.org/packages/67/d5/38da808f968a54b0fead2d7e15ca011d3df93c96a07f4914e8ef3974506e/ndindex-1.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3307817bdc92846b18f309fae3582856f567dd6e0742fb0b41ac68682bfc4e2a", size = 1491141, upload-time = "2025-11-19T20:38:35.785Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/8c66ef982a01ae4cbdabba679a2bc711f262cedf23bfb9682293146f8a98/ndindex-1.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae73cd2d66b09ef2f2a7d7f93bad396d6abf168d1ee825e403c6c5fb8ae1341c", size = 1543876, upload-time = "2025-11-19T20:38:37.456Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/7c7e3a3c6e81b4284fd0d53cbaec51d9e5b90df26dd78e9bde06cb307217/ndindex-1.10.1-cp311-cp311-win32.whl", hash = "sha256:890bb92f0a779e6f16bdbcc8bd2e06c32bcc0239e5893ba246114eb924aecaaa", size = 149149, upload-time = "2025-11-19T20:38:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/99e1fb0effdef74b883be615ea0053ebcea28a53fd8b896263f4e99b0113/ndindex-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:1827a40301405b44ad709e388c5b48cf35cd90a67f77e63f0f17d87f6000fa81", size = 157246, upload-time = "2025-11-19T20:38:40.197Z" }, + { url = "https://files.pythonhosted.org/packages/65/90/774ddd08b2a1b41faa56da111f0fbfeb4f17ee537214c938ef41d61af949/ndindex-1.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87f83e8c35a7f49a68cd3a3054c406e6c22f8c1315f3905f7a778c657669187e", size = 177348, upload-time = "2025-11-19T20:38:41.768Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ee/a423e857f5b45da3adc8ddbcfbfd4a0e9a047edce3915d3e3d6e189b6bd9/ndindex-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf9e05986b2eb8c5993bce0f911d6cedd15bda30b5e35dd354b1ad1f4cc3599d", size = 176561, upload-time = "2025-11-19T20:38:43.06Z" }, + { url = "https://files.pythonhosted.org/packages/1f/40/139b6b050ba2b2a0bb40e0381a352b1eb6551302dcb8f86fb4c97dd34e92/ndindex-1.10.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046c1e88d46b2bd2fd3483e06d27b4e85132b55bc693f2fca2db0bb56eea1e78", size = 542901, upload-time = "2025-11-19T20:38:44.43Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/defd665dbbeb2fffa077491365ed160acaec49274ce8d4b979f55db71f18/ndindex-1.10.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03cf1e6cdac876bd8fc92d3b65bb223496b1581d10eab3ba113f7c195121a959", size = 546875, upload-time = "2025-11-19T20:38:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/6d54d48e8eaee25cdab70d3e4c4f579ddb0255e4f1660040d5ad55e029c6/ndindex-1.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:752e78a5e87911ded117c57a7246596f26c9c6da066de3c2b533b3db694949bb", size = 1510036, upload-time = "2025-11-19T20:38:47.444Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/e28ba3b98eacd18193176526526b34d7d70d2a6f9fd2b4d8309ab5692678/ndindex-1.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9dd58d91220b1c1fe516324bfcf4114566c98e84b1cbbe416abe345c75bd557", size = 1571849, upload-time = "2025-11-19T20:38:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/8f/63/83fff78a3712cb9f478dd84a19ec389acf6f8c7b01dc347a65ae74e6123d/ndindex-1.10.1-cp312-cp312-win32.whl", hash = "sha256:3b0d9ce2c8488444499ab6d40e92e09867bf4413f5cf04c01635de923f44aa67", size = 149792, upload-time = "2025-11-19T20:38:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/52/fd/a5e3c8c043d0dddea6cd4567bfaea568f022ac197301882b3d85d9c1e9b3/ndindex-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c026dbbf2455d97ce6456d8a50b349aee8fefa11027d020638c89e9be2c9c4c", size = 158164, upload-time = "2025-11-19T20:38:52.242Z" }, + { url = "https://files.pythonhosted.org/packages/60/ea/03676266cb38cc671679a9d258cc59bfc58c69726db87b0d6eeafb308895/ndindex-1.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:157b5c34a1b779f5d27b790d9bd7e7b156d284e76be83c591a3ba003984f4956", size = 176323, upload-time = "2025-11-19T20:38:53.528Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2d350439031b108b0bb8897cad315390c5ad88c14d87419a54c2ffa95c80/ndindex-1.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f99b3e89220da3244d03c9c5473669c7107d361c129fd9b064622744dee1ce15", size = 175584, upload-time = "2025-11-19T20:38:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/a51b7c6f7159718a6a0a694fc1058b94d793c416d9a4fd649f1924cce5f8/ndindex-1.10.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6928e47fb008903f2e41309b7ff1e59b16abbcd59e2e945454571c28b2433c9e", size = 524127, upload-time = "2025-11-19T20:38:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/d8f19f0b8fc9c5585b50fda44c05415da0bdc5fa9c9c69011015dac27880/ndindex-1.10.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69a2cb1ac7be955c3c77f1def83f410775a81525c9ce2d4c0a3f2a61589ed47", size = 528213, upload-time = "2025-11-19T20:39:00.882Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/77d9d037e871a3faa8579b354ca2dd09cc5bbf3e085d9e3c67f786d55ee3/ndindex-1.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cb76e0f3f235d8b1c768b17e771de48775d281713795c3aa045e8114ad61bdda", size = 1492172, upload-time = "2025-11-19T20:39:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/ad13676fc9312e0aa1a80a7c04bcb0b502b877ed4956136117ad663eced0/ndindex-1.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7da34a78410c14341d5fff73be5ce924bd36500bf7f640fc59b8607d3a0df95e", size = 1552614, upload-time = "2025-11-19T20:39:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/e6e6fd81423810c07ae623c4d36e099f42a812994977e8e3bfa182c02472/ndindex-1.10.1-cp313-cp313-win32.whl", hash = "sha256:9599fcb7411ffe601c367f0a5d4bc0ed588e3e7d9dc7604bdb32c8f669456b9e", size = 149330, upload-time = "2025-11-19T20:39:05.727Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d3/830a20626e2ec0e31a926be90e67068a029930f99e6cfebf2f9768e7b7b1/ndindex-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:ef3ef22390a892d16286505083ee5b326317b21c255a0c7f744b1290a0b964a6", size = 157309, upload-time = "2025-11-19T20:39:07.394Z" }, + { url = "https://files.pythonhosted.org/packages/4a/73/3bdeecd1f6ec0ad81478a53d96da4ba9be74ed297c95f2b4fbe2b80843e1/ndindex-1.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:72af787dcee3661f36fff9d144d989aacefe32e2c8b51ceef9babd46afb93a18", size = 181022, upload-time = "2025-11-19T20:39:10.487Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b1/0d97ba134b5aa71b5ed638fac193a7ec4d987e091e2f4e4162ebdaacbda1/ndindex-1.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa60637dfae1ee3fc057e420a52cc4ace38cf2c0d1a0451af2a3cba84d281842", size = 181289, upload-time = "2025-11-19T20:39:11.793Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d7/1df02df24880ce3f3c8137b6f3ca5a901a58d9079dcfd8c818419277ff87/ndindex-1.10.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ebdba2fade3f6916fe21fd49e2a0935af4f58c56100a60f3f2eb26e20baee7", size = 632517, upload-time = "2025-11-19T20:39:13.259Z" }, + { url = "https://files.pythonhosted.org/packages/34/96/b509c2b14e9b10710fe6ab6ba8bda1ee6ce36ab16397ff2f5bbb33bbbba3/ndindex-1.10.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:346a4bf09f5771548665c8206e81daadb6b9925d409746e709894bdd98adc701", size = 616179, upload-time = "2025-11-19T20:39:14.757Z" }, + { url = "https://files.pythonhosted.org/packages/38/e3/f89d60cf351c33a484bf1a4546a5dee6f4e7a6a973613ffa12bd316b14ad/ndindex-1.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:23d35696f802548143b5cc199bf2f171efb0061aa7934959251dd3bae56d038c", size = 1588373, upload-time = "2025-11-19T20:39:16.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/002fc1e6a4abeef8d92e9aa2e43aea4d462f6b170090f7752ea8887f4897/ndindex-1.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a91e1a0398120233d5c3b23ccb2d4b78e970d66136f1a7221fa9a53873c3d5c5", size = 1636436, upload-time = "2025-11-19T20:39:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/28b1ad78c787ac8fafd6e26419a80366617784b1779e3857fa687492f6bc/ndindex-1.10.1-cp313-cp313t-win32.whl", hash = "sha256:78bfe25941d2dac406391ddd9baf0b0fce163807b98ecc2c47a3030ee8466319", size = 158780, upload-time = "2025-11-19T20:39:20.454Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/b81060607a19865bb8be8d705b1b3e8aefb8747c0fbd383e38b4cae4bd71/ndindex-1.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:08bfdc1f7a0b408d15b3ce61d141ebbebdb47a25341967e425e104c5bd512a5c", size = 167485, upload-time = "2025-11-19T20:39:21.733Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/aac1131e9f3a5635ba7b0312c3bfa610511ab4108f85c0d914a32887aa00/ndindex-1.10.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b5297f207ebc068c7cdf9e3cd7b95aa5c9ec04295d0a7e56b529f66787d4685", size = 176478, upload-time = "2025-11-19T20:39:23.747Z" }, + { url = "https://files.pythonhosted.org/packages/1a/05/a0d8ca0432c84550bc17af6d6479a803936895b8b8403a1216c5a55475fb/ndindex-1.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c5e9762452b163e33cfb6e821f86e45ba0b53bdfcd23ab5d57b48a8f566898cb", size = 175480, upload-time = "2025-11-19T20:39:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/09/4a/028ab78a9f29fd2a7e86a90337cde4658eaa77b425c63045d83a1d2e4f26/ndindex-1.10.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf80241b40adffdc3276b2c9fb63a96c6c98b4a9d941892738de8add65083962", size = 528125, upload-time = "2025-11-19T20:39:26.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/bd823b345fb06c83ade6ef1c1933521d4357cd04490e684d4fa30126926c/ndindex-1.10.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf5855881884b8467dfcf45764ccf2e4279075be14b155b89c96994bb08d2e6f", size = 527328, upload-time = "2025-11-19T20:39:28.292Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/40b9c15588cbf9dde43c4fb88a31dd1f636a913fa29649f18f8e3ebca36a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e81a9bd36fe054b6c9fcc53d26bc9a28cf15d1ab52a0f5b854f894116f3a54e1", size = 1497508, upload-time = "2025-11-19T20:39:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/24/8f/b8048f7837d2e9dff0af507b398307fa84a2aa9ea3db71b4aa800b21da4a/ndindex-1.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:588e8875d836a93b3cd9af482c8074bb02288ae1aff92cf277e1f02d9ae0f992", size = 1552625, upload-time = "2025-11-19T20:39:32.404Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/0ecb53c7e690a44769f2f92a843723ccb1d0ce080d93ba1ea811304cca12/ndindex-1.10.1-cp314-cp314-win32.whl", hash = "sha256:28741daca5926adff402247cd406f453ed5bb6042e82d6855938f805190e5ce9", size = 151237, upload-time = "2025-11-19T20:39:34.847Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4e/197982fa8b4e6e6b9d15c38505c41076d1c552921f09f4d35acbbbbc0b70/ndindex-1.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:59a3222befc0f7cdc85fb9b90a567ae890f70a864bdeb660517e9ebcb36bf1bc", size = 158925, upload-time = "2025-11-19T20:39:37.149Z" }, + { url = "https://files.pythonhosted.org/packages/24/ad/116b6154046a69fc04e2d4490905801d3839a3f21290c0b4d49b1044e251/ndindex-1.10.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967b87b88dadb62555ec1039695c347254eccb8ca3d124c0e5dbe084c525fa93", size = 181724, upload-time = "2025-11-19T20:39:38.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/00/3ce4351366c890bcc87a5e9f1f90102547962eef356ac7c799bfdd0dddce/ndindex-1.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c67dde588c0fb89d872931a4ed5f9b4d21c1c70a3d92fdf0812a1de154239816", size = 181653, upload-time = "2025-11-19T20:39:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/a6fda696a2f02a3f8dd2ee9d816cb2edff6423bf0110a4876cc3b1259732/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65ca639a7abf72d79f22424f4abd18dece1f289a2b7b028a0ca455edd2168d4", size = 630898, upload-time = "2025-11-19T20:39:41.495Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/eb2e5d067d4c054451e33eaece74cbdcb58236dc60516e73d783dae34c7e/ndindex-1.10.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3634a8df43e7928122225a3d64d850c8957bd1edf2e403907deacb478af27b", size = 614419, upload-time = "2025-11-19T20:39:43.254Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/261bfb49eb7920c2a7314cacba5821930a529911dce48c7c6cd786096a5a/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d581f931e61f182478f18bdf5edd3955899df5da4892ed0d5de547a4cfd5b6f", size = 1587517, upload-time = "2025-11-19T20:39:44.809Z" }, + { url = "https://files.pythonhosted.org/packages/ec/37/084a332ecdf8b0049151bd78001a7baf2daf7f500d043beb8a1f95d0f4e3/ndindex-1.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:78ce45106ebf67aeba99714818c721d8fd5fb9534daebd2565665a2d64b50fc9", size = 1635372, upload-time = "2025-11-19T20:39:47.231Z" }, + { url = "https://files.pythonhosted.org/packages/28/f4/716580fbb03018ab1daa86ed12c1925c67e79689db5fee82393e840758a2/ndindex-1.10.1-cp314-cp314t-win32.whl", hash = "sha256:fe5341e24dc992b09c258456ac90a09a6d25efdc2cb86dcc91d32c8891e1df9a", size = 162186, upload-time = "2025-11-19T20:39:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/4d/20/28f669c09a470e7f523b0cc10b94336664d9648594015e3f2a1ec29047b1/ndindex-1.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:37f87f0e7690ae0324334740e0661d6297f2e62c9bf925127d249fb7eddd0ad8", size = 171077, upload-time = "2025-11-19T20:39:50.108Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "npe2" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "platformdirs" }, + { name = "psygnal" }, + { name = "pydantic" }, + { name = "pydantic-extra-types" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tomli-w" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/0a/1a6ed44b038484a064a0ac8e5f96aae6ddb8768789145cf7e65e699b324b/npe2-0.8.2.tar.gz", hash = "sha256:6e41cc1f2c873257d864980dd281b5bb649a84cef02feeb0cdda1a9d23fd8f8b", size = 122768, upload-time = "2026-04-04T20:55:11.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a6/f318fbc7bbb62361d36cb694f399f765f57fb282fdadc3aa2fef4dafd62d/npe2-0.8.2-py3-none-any.whl", hash = "sha256:80eff5fef50352cf0f3407c4c1122a7ae186aede40a21fd595cf91988cd55a9d", size = 93921, upload-time = "2026-04-04T20:55:10.552Z" }, +] + +[[package]] +name = "numba" +version = "0.65.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1b/3c5a7daf683a95465bf23504bcd1a2d5db8cd5e5e276ca87505d020dffe9/numba-0.65.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9d993ed0a257aa4116e6f553f114004bcfdee540c7276ab8ea48f650d514c452", size = 2680870, upload-time = "2026-04-24T02:02:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/1831836814018a898e7d252aebe09c0f3ce1f26d145b68264b4ae0be6822/numba-0.65.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f098109f361681e57295f7e84d8ab2426902539a141811de0703ace52826981", size = 3739780, upload-time = "2026-04-24T02:02:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/a813ddc81def09e257d2b1f67521982ce4b06204a87268796ffc8187271c/numba-0.65.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973fd8173f2312815e6b7aaae887c4ce8a817eeff46a4f8840b828305b75bc95", size = 3446722, upload-time = "2026-04-24T02:02:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/ee1d8b3becda384fe0552221641e05aa668a35e8a77470db4db7f6475000/numba-0.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:c63aa0c4193694026452da55d0ef9d85156c1a7a333454c103bb30dec81b7bf8", size = 2747539, upload-time = "2026-04-24T02:02:16.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7020d74b19cdb8cff16506542fdd510756e28c5e7f3bd0b7f574f0f42272fcd9", size = 2680563, upload-time = "2026-04-24T02:02:18.414Z" }, + { url = "https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420", size = 3745018, upload-time = "2026-04-24T02:02:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/49/aa/4361698f35bf63bff67dfe6c90493731177f48ede954f77b0588731537bc/numba-0.65.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ed425a43b0a5f9772f2f4e2dd0bbd12eabecae1af0b24efcfd4e053f012aac6", size = 3450962, upload-time = "2026-04-24T02:02:22.449Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:df40a5028a975b9ea66f6a2a3f7abbdbd541a863070e34ed367aff21141248e4", size = 2747417, upload-time = "2026-04-24T02:02:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac3f1e77c352dd0ea9712732c2d8f9ca507717435eec5b5013bf138ac33c4a08", size = 2681371, upload-time = "2026-04-24T02:02:26.105Z" }, + { url = "https://files.pythonhosted.org/packages/69/47/a415af0283e4db0398104c6d1c11c9861a98dc67a7aa442a7769ed5d6196/numba-0.65.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52bc6f3ceb8fcaff9b2ae26b4c6b1e9fee39db8d355534c0fe4f39a901246b84", size = 3802467, upload-time = "2026-04-24T02:02:27.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/36/246f73ec99cfeab2f2cb2ce7d4218766cc36a2da418901223f4f4da9c813/numba-0.65.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ca10b3463bae0bd70589726fe3c77d01d6b5fc86bee54bcdf9fb6b47c28977", size = 3502628, upload-time = "2026-04-24T02:02:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/db/9e/3c679b2ee078425b9e99a91e44f8d132a6830d8ccce5227bc5e9181aeed8/numba-0.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:5971c632be2a2351500431f46213821dba8d02b18a9f7d02fd36bd2743e41a6a", size = 2750611, upload-time = "2026-04-24T02:02:31.477Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3", size = 2681344, upload-time = "2026-04-24T02:02:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, +] + +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/91/ccd504cbe5b88d06987c77f42ba37a13ef05065fdab4afe6dcfeb2961faf/numexpr-2.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0fab3fd06a04f6b86102552b26aa5d85e20ac7d8296c15764c726eeabae6cc8", size = 163200, upload-time = "2025-10-13T16:16:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/f3/89/6b07977baf2af75fb6692f9e7a1fb612a15f600fc921f3f565366de01f4a/numexpr-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:64ae5dfd62d74a3ef82fe0b37f80527247f3626171ad82025900f46ffca4b39a", size = 152085, upload-time = "2025-10-13T16:16:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/c2/c5775541256c4bf16b4d88fa1cffa74a0126703e513093c8774d911b0bb7/numexpr-2.14.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:955c92b064f9074d2970cf3138f5e3b965be673b82024962ed526f39bc25a920", size = 449435, upload-time = "2025-10-13T16:13:16.257Z" }, + { url = "https://files.pythonhosted.org/packages/34/d4/d1a410901c620f7a6a3c5c2b1fc9dab22170be05a89d2c02ae699e27bd3f/numexpr-2.14.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75440c54fc01e130396650fdf307aa9d41a67dc06ddbfb288971b591c13a395b", size = 440197, upload-time = "2025-10-13T16:14:44.109Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c8/fa85f0cc5c39db587ba4927b862a92477c017ee8476e415e8120a100457b/numexpr-2.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dde9fa47ed319e1e1728940a539df3cb78326b7754bc7c6ab3152afc91808f9b", size = 1414125, upload-time = "2025-10-13T16:13:19.882Z" }, + { url = "https://files.pythonhosted.org/packages/08/72/a58ddc05e0eabb3fa8d3fcd319f3d97870e6b41520832acfd04a6734c2c0/numexpr-2.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76db0bc6267e591ab9c4df405ffb533598e4c88239db7338d11ae9e4b368a85a", size = 1463041, upload-time = "2025-10-13T16:14:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/bdd1862302bb71a78dba941eaf7060e1274f1cf6af2d1b0f1880bfcb289b/numexpr-2.14.1-cp310-cp310-win32.whl", hash = "sha256:0d1dcbdc4d0374c0d523cee2f94f06b001623cbc1fd163612841017a3495427c", size = 166833, upload-time = "2025-10-13T16:17:03.543Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/26773a246716922794388786529e5640676399efabb0ee217ce034df9d27/numexpr-2.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:823cd82c8e7937981339f634e7a9c6a92cb2d0b9d0a5cf627a5e394fffc05377", size = 160068, upload-time = "2025-10-13T16:17:05.191Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195, upload-time = "2025-10-13T16:16:31.212Z" }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088, upload-time = "2025-10-13T16:16:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838, upload-time = "2025-10-13T16:17:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069, upload-time = "2025-10-13T16:17:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794, upload-time = "2025-10-13T16:16:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203, upload-time = "2025-10-13T16:16:39.907Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860, upload-time = "2025-10-13T16:13:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574, upload-time = "2025-10-13T16:15:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306, upload-time = "2025-10-13T16:13:42.813Z" }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145, upload-time = "2025-10-13T16:15:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996, upload-time = "2025-10-13T16:17:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189, upload-time = "2025-10-13T16:17:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524, upload-time = "2025-10-13T16:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919, upload-time = "2025-10-13T16:16:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972, upload-time = "2025-10-13T16:13:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953, upload-time = "2025-10-13T16:15:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199, upload-time = "2025-10-13T16:13:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "numpydoc" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/45/9e/2f562daf80eb8f7a685fb7bea4fda71f6048e4f359d6fdd1b6e70206cb2f/nvidia_cublas-13.1.1.3-py3-none-win_amd64.whl", hash = "sha256:b6cdce694e47ff6aadf0a69df1cab6628d696f5ff56e8d16af50309d855fa20f", size = 404358158, upload-time = "2026-04-08T18:47:26.987Z" }, +] + +[[package]] +name = "nvidia-cublas-cu11" +version = "11.10.3.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/41/fdeb62b5437996e841d83d7d2714ca75b886547ee8017ee2fe6ea409d983/nvidia_cublas_cu11-11.10.3.66-py3-none-manylinux1_x86_64.whl", hash = "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded", size = 317097917, upload-time = "2022-08-11T17:32:30.789Z" }, + { url = "https://files.pythonhosted.org/packages/7a/08/57e6b6481af73590259a9600c32a68eb853966e354fca147cde17ed9ea27/nvidia_cublas_cu11-11.10.3.66-py3-none-win_amd64.whl", hash = "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e", size = 311065222, upload-time = "2022-08-03T21:16:08.044Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/61/7d7b3c70186fb651d0fbd35b01dbfc8e755f69fd58f817f3d0f642df20c3/nvidia_cublas_cu12-12.8.4.1-py3-none-win_amd64.whl", hash = "sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af", size = 567544208, upload-time = "2025-03-07T01:53:30.535Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/b74b10025c1205695c5676373f2edd3e87a7202cc62ead0dfbc373b0f6ea/nvidia_cuda_cupti-13.0.85-py3-none-win_amd64.whl", hash = "sha256:683f58d301548deeefcb8f6fac1b8d907691b9d8b18eccab417f51e362102f00", size = 7736776, upload-time = "2025-09-04T08:38:08.38Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu11" +version = "11.7.101" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/9d/dd0cdcd800e642e3c82ee3b5987c751afd4f3fb9cc2752517f42c3bc6e49/nvidia_cuda_cupti_cu11-11.7.101-py3-none-manylinux1_x86_64.whl", hash = "sha256:e0cfd9854e1f2edaa36ca20d21cd0bdd5dcfca4e3b9e130a082e05b33b6c5895", size = 11845698, upload-time = "2022-08-03T20:58:36.733Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/f314475cc4740b6138daf9c6496b165bab1f07c161bd4ac5e69285ab07d6/nvidia_cuda_cupti_cu11-11.7.101-py3-none-win_amd64.whl", hash = "sha256:7cc5b8f91ae5e1389c3c0ad8866b3b016a175e827ea8f162a672990a402ab2b0", size = 8461264, upload-time = "2022-08-03T21:14:45.554Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/83f5426095d93694ae39fe1311431b5d5a9bb82e48bf0dd8e19be2765942/nvidia_cuda_cupti_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e", size = 7015759, upload-time = "2025-03-07T01:51:11.355Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/4a/af/345fedb9f4c76c84ab4fa445b36bd4048a4d9db60e6bc76b4f913ff4b852/nvidia_cuda_nvrtc-13.0.88-py3-none-win_amd64.whl", hash = "sha256:6bcd4e7f8e205cbe644f5a98f2f799bef9556fefc89dd786e79a16312ce49872", size = 76807835, upload-time = "2025-09-04T08:39:15.274Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu11" +version = "11.7.99" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/25/922c5996aada6611b79b53985af7999fc629aee1d5d001b6a22431e18fec/nvidia_cuda_nvrtc_cu11-11.7.99-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03", size = 21011023, upload-time = "2022-09-21T23:12:53.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8d/0709ba16c2831c17ec1c2ea1eeb89ada11ffa8d966d773cce0a7463b22bb/nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7", size = 21010447, upload-time = "2022-08-03T20:59:13.991Z" }, + { url = "https://files.pythonhosted.org/packages/9e/10/c9fc448f33d439981d6a74b693526871c4ef13e8d81a7b4de12e3a12a1b9/nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3", size = 17040212, upload-time = "2022-08-03T21:15:19.801Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/45/51/52a3d84baa2136cc8df15500ad731d74d3a1114d4c123e043cb608d4a32b/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909", size = 73586838, upload-time = "2025-03-07T01:52:13.483Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/b7/94/6b867483bec07da24ffa32736c79fabb94ef3a7af4d787a9d4a974868576/nvidia_cuda_runtime-13.0.96-py3-none-win_amd64.whl", hash = "sha256:f79298c8a098cec150a597c8eba58ecdab96e3bdc4b9bc4f9983635031740492", size = 2927037, upload-time = "2025-10-09T09:04:23.782Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu11" +version = "11.7.99" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/92/89cf558b514125d2ebd8344dd2f0533404b416486ff681d5434a5832a019/nvidia_cuda_runtime_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31", size = 849253, upload-time = "2022-08-03T20:58:27.979Z" }, + { url = "https://files.pythonhosted.org/packages/32/2c/d89ea2b4051fbabff8d2edda8c735dabae6d5d1b8d5215f9749d38dcdb72/nvidia_cuda_runtime_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7", size = 991354, upload-time = "2022-08-03T21:14:37.958Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, + { url = "https://files.pythonhosted.org/packages/30/a5/a515b7600ad361ea14bfa13fb4d6687abf500adc270f19e89849c0590492/nvidia_cuda_runtime_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8", size = 944318, upload-time = "2025-03-07T01:51:01.794Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu11" +version = "8.5.0.96" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/30/66d4347d6e864334da5bb1c7571305e501dcb11b9155971421bb7bb5315f/nvidia_cudnn_cu11-8.5.0.96-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7", size = 557141533, upload-time = "2022-09-21T21:55:09.845Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/4d28d4706946f89fffe3f87373a079ae95dc17f9c0fcd840fe570c67e36b/nvidia_cudnn_cu11-8.5.0.96-py3-none-manylinux1_x86_64.whl", hash = "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108", size = 557140881, upload-time = "2022-08-10T00:14:42.613Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/3d/90/0bd6e586701b3a890fd38aa71c387dab4883d619d6e5ad912ccbd05bfd67/nvidia_cudnn_cu12-9.10.2.21-py3-none-win_amd64.whl", hash = "sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e", size = 692992268, upload-time = "2025-06-06T21:55:18.114Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/78/39/21507455b1bca8b5702a9e9fc6ce73735f216f558dac2c9ede58e4d456b8/nvidia_cudnn_cu13-9.20.0.48-py3-none-win_amd64.whl", hash = "sha256:af8139732b99c0118be65ea5aac97f0d46018f8c552889e49d2fb0c6261a4a24", size = 350712614, upload-time = "2026-03-09T19:31:11.398Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/f8af21a2ed1beed337a6a02c5a28aeb85441f4d578ec3d529543c775ea4b/nvidia_cufft-12.0.0.61-py3-none-win_amd64.whl", hash = "sha256:2abce5b39d2f5ae12730fb7e5db6696533e36c26e2d3e8fd1750bdd2853364eb", size = 213342123, upload-time = "2025-09-04T08:40:51.145Z" }, +] + +[[package]] +name = "nvidia-cufft-cu11" +version = "10.9.0.58" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/79/b912a77e38e41f15a0581a59f5c3548d1ddfdda3225936fb67c342719e7a/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux1_x86_64.whl", hash = "sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81", size = 168405414, upload-time = "2022-10-03T23:29:47.505Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/a2ad9951d57c3cc23f4fa6d84b146afd9f375ffbc744b38935930ac4393f/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux2014_aarch64.whl", hash = "sha256:34b7315104e615b230dc3c2d1861f13bff9ec465c5d3b4bb65b4986d03a1d8d4", size = 111231060, upload-time = "2024-08-17T00:00:57.04Z" }, + { url = "https://files.pythonhosted.org/packages/64/c8/133717b43182ba063803e983e7680a94826a9f4ff5734af0ca315803f1b3/nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e21037259995243cc370dd63c430d77ae9280bedb68d5b5a18226bfc92e5d748", size = 168405419, upload-time = "2024-08-17T00:02:03.562Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b4/e432a74f8db0e84f734dc14d36c0e529225132bf7e239da21f55893351a6/nvidia_cufft_cu11-10.9.0.58-py3-none-win_amd64.whl", hash = "sha256:c4d316f17c745ec9c728e30409612eaf77a8404c3733cdf6c9c1569634d1ca03", size = 172237004, upload-time = "2022-10-03T23:39:58.288Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/ce1629f1e478bb5ccd208986b5f9e0316a78538dd6ab1d0484f012f8e2a1/nvidia_cufft_cu12-11.3.3.83-py3-none-win_amd64.whl", hash = "sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7", size = 192216559, upload-time = "2025-03-07T01:53:57.106Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, + { url = "https://files.pythonhosted.org/packages/99/27/72103153b1ffc00e09fdc40ac970235343dcd1ea8bd762e84d2d73219ffa/nvidia_curand-10.4.0.35-py3-none-win_amd64.whl", hash = "sha256:65b1710aa6961d326b411e314b374290904c5ddf41dc3f766ebc3f1d7d4ca69f", size = 55242481, upload-time = "2025-08-04T10:30:41.831Z" }, +] + +[[package]] +name = "nvidia-curand-cu11" +version = "10.2.10.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/11/af78d54b2420e64a4dd19e704f5bb69dcb5a6a3138b4465d6a48cdf59a21/nvidia_curand_cu11-10.2.10.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:eecb269c970fa599a2660c9232fa46aaccbf90d9170b96c462e13bcb4d129e2c", size = 54628716, upload-time = "2022-08-03T21:13:08.944Z" }, + { url = "https://files.pythonhosted.org/packages/45/76/b98f30e058c9bbd9a56eb9b1102b9aab775704bad9286bf8e3998147e2e9/nvidia_curand_cu11-10.2.10.91-py3-none-win_amd64.whl", hash = "sha256:f742052af0e1e75523bde18895a9ed016ecf1e5aa0ecddfcc3658fd11a1ff417", size = 54342083, upload-time = "2022-08-03T21:17:21.537Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, + { url = "https://files.pythonhosted.org/packages/b9/75/70c05b2f3ed5be3bb30b7102b6eb78e100da4bbf6944fd6725c012831cab/nvidia_curand_cu12-10.3.9.90-py3-none-win_amd64.whl", hash = "sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec", size = 62765309, upload-time = "2025-03-07T01:54:20.478Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, + { url = "https://files.pythonhosted.org/packages/99/ef/332a0101260ca78a1daef046bf0b06199e8ed4dac1d2aa698289c358169c/nvidia_cusolver-12.0.4.66-py3-none-win_amd64.whl", hash = "sha256:16515bd33a8e76bb54d024cfa068fa68d30e80fc34b9e1090813ea9362e0cb65", size = 193551444, upload-time = "2025-09-04T08:41:46.813Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu11" +version = "11.4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/77/66149e3153b19312fb782ea367f3f950123b93916a45538b573fe373570a/nvidia_cusolver_cu11-11.4.0.1-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:72fa7261d755ed55c0074960df5904b65e2326f7adce364cbe4945063c1be412", size = 102594907, upload-time = "2022-09-21T23:13:22.001Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/272f9aa7838e545b47878e4aec4f09b0fecf17dbd312cf5c5dc398b0637f/nvidia_cusolver_cu11-11.4.0.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:700b781bfefd57d161443aff9ace1878584b93e0b2cfef3d6e9296d96febbf99", size = 102592389, upload-time = "2022-08-03T21:13:24.499Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/f682b0b001562d16664bd7b015165cf2c2d392a8d0506472f28b2833953e/nvidia_cusolver_cu11-11.4.0.1-py3-none-win_amd64.whl", hash = "sha256:00f70b256add65f8c1eb3b6a65308795a93e7740f6df9e273eccbba770d370c4", size = 100095353, upload-time = "2022-08-03T21:17:36.992Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/76ca8551b8a84146ffa189fec81c26d04adba4bc0dbe09cd6e6fd9b7de04/nvidia_cusolver_cu12-11.7.3.90-py3-none-win_amd64.whl", hash = "sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34", size = 256720438, upload-time = "2025-03-07T01:54:39.898Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, + { url = "https://files.pythonhosted.org/packages/02/b0/b043d6f3480f102f885cf87fc3ffd3edcb5e23b855025a50e2ef4d059185/nvidia_cusparse-12.6.3.3-py3-none-win_amd64.whl", hash = "sha256:cbcf42feb737bd7ec15b4c0a63e62351886bd3f975027b8815d7f720a2b5ea79", size = 143783033, upload-time = "2025-09-04T08:42:12.391Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu11" +version = "11.7.4.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/6f/6d032cc1bb7db88a989ddce3f4968419a7edeafda362847f42f614b1f845/nvidia_cusparse_cu11-11.7.4.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:a3389de714db63321aa11fbec3919271f415ef19fda58aed7f2ede488c32733d", size = 173182291, upload-time = "2022-08-03T21:13:55.407Z" }, + { url = "https://files.pythonhosted.org/packages/15/3e/d32da819d918b0b9ef3fa89ed8238d2c3b8e315ac32441229783a4b0c4ce/nvidia_cusparse_cu11-11.7.4.91-py3-none-win_amd64.whl", hash = "sha256:304a01599534f5186a8ed1c3756879282c72c118bc77dd890dc1ff868cad25b9", size = 172505954, upload-time = "2022-08-03T21:18:07.425Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, + { url = "https://files.pythonhosted.org/packages/62/07/f3b2ad63f8e3d257a599f422ae34eb565e70c41031aecefa3d18b62cabd1/nvidia_cusparse_cu12-12.5.8.93-py3-none-win_amd64.whl", hash = "sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd", size = 284937404, upload-time = "2025-03-07T01:55:07.742Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d8/a6b0d0d0c2435e9310f3e2bb0d9c9dd4c33daef86aa5f30b3681defd37ea/nvidia_cusparselt_cu12-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075", size = 271020911, upload-time = "2025-02-26T00:14:47.204Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/31/83/f3647ce26916c94a6ca4ff1810623e2c405cff2dea6e78d29516b2514df9/nvidia_cusparselt_cu13-0.8.1-py3-none-win_amd64.whl", hash = "sha256:dccbd362f91a7b9024d1f55ee9f548ac065027ff15d8c8b0db889ab3a8f31215", size = 156885108, upload-time = "2025-09-05T18:51:35.958Z" }, +] + +[[package]] +name = "nvidia-nccl-cu11" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/92/914cdb650b6a5d1478f83148597a25e90ea37d739bd563c5096b0e8a5f43/nvidia_nccl_cu11-2.14.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:5e5534257d1284b8e825bc3a182c6f06acd6eb405e9f89d49340e98cd8f136eb", size = 177099966, upload-time = "2022-08-31T23:13:15.915Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, + { url = "https://files.pythonhosted.org/packages/e4/01/07530b0e37546231052e30234540289c42eaffa486f1a34a87fed340157b/nvidia_nvjitlink-13.0.88-py3-none-win_amd64.whl", hash = "sha256:634e96e3da9ef845ae744097a1f289238ecf946ce0b82e93cdce14b9782e682f", size = 36035115, upload-time = "2025-09-04T08:43:03.001Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d7/34f02dad2e30c31b10a51f6b04e025e5dd60e5f936af9045a9b858a05383/nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f", size = 268553710, upload-time = "2025-03-07T01:56:24.13Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d2/50/0e2220f8620a177de994211186ffc5bfa9f2ce1e1282797f8f90096f9f88/nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519", size = 137066, upload-time = "2025-09-04T08:39:25.649Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu11" +version = "11.7.91" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wheel", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/d5/09493ff0e64fd77523afbbb075108f27a13790479efe86b9ffb4587671b5/nvidia_nvtx_cu11-11.7.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:b22c64eee426a62fc00952b507d6d29cf62b4c9df7a480fcc417e540e05fd5ac", size = 98579, upload-time = "2022-08-03T20:59:22.605Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e1/37b5a5da8ec3594890356e9d60617feb36cfea1223ac511a78c615870916/nvidia_nvtx_cu11-11.7.91-py3-none-win_amd64.whl", hash = "sha256:dfd7fcb2a91742513027d63a26b757f38dd8b07fecac282c4d132a9d373ff064", size = 65886, upload-time = "2022-08-03T21:15:27.865Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/9f/99/4c9c0c329bf9fc125008c3b54c7c94c0023518d06fc025ae36431375e1fe/nvidia_nvtx_cu12-12.8.90-py3-none-win_amd64.whl", hash = "sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e", size = 56492, upload-time = "2025-03-07T01:52:24.69Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, +] + +[[package]] +name = "openvino-dev" +version = "2022.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/99/9e55ddb1abce5ff7def768470810044a6de48a5da99b9195498ad75dfe8a/openvino_dev-2022.1.0-7019-py3-none-any.whl", hash = "sha256:3d911b31a89e92dcb92a29026cfb396920211aca9525ea50032419f10169098d", size = 5774377, upload-time = "2022-03-22T13:07:17.333Z" }, +] + +[[package]] +name = "opt-einsum" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, +] + +[[package]] +name = "optree" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531, upload-time = "2026-05-06T02:32:39.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/48/53367634a0ab6c2f0e502d83f8d6e27b70b6848ff1e1ff9cf042d1e1f1a0/optree-0.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1b28b0d89def1b4554051f3de2a1ed81e20216b6454a59a0d16c9f55c08cff77", size = 398400, upload-time = "2026-05-06T02:30:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/350c82cd77a510f0f495e38a6f333b4b45a413dbc224142bc59975bc09d6/optree-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14f959bc6bea6e0532f9239c67ea6952f3b8d0755ea9b4dd498284b649275aba", size = 370049, upload-time = "2026-05-06T02:30:33.065Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/81b660daea2a75f574549e62c198d0b4e8e148b5de6f5f72e90a5cc1c334/optree-0.19.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1687c962bb1691525178a6e90dde5840197cd7a7ad914b407eb7b635f15d47cb", size = 390143, upload-time = "2026-05-06T02:30:34.585Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ec/ee009b5a31227b089d72fec2af3bb6bc0efd95bbe87ffe46f11061b9d371/optree-0.19.1-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d7edead66cace8b3b905488e391b38487614f75ae4fa7f3b612c7fe0e54b8a90", size = 445740, upload-time = "2026-05-06T02:30:35.891Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/2fe8ac73b7e979f3ed477ad99b7e034a11207d728b84ed2f52da259e7cda/optree-0.19.1-cp310-cp310-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9fb767746231ff279d273e8ff71af2a8f89c0c3870ca367c45fd4526d331ae4b", size = 446631, upload-time = "2026-05-06T02:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/489fb272de36e0233149d46887879deb9497edc4a0214674bd2a80b8d4ec/optree-0.19.1-cp310-cp310-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75fe038a1bed44f487a084af7a978874c51bba55f850bc12bf8068f3242463d6", size = 442053, upload-time = "2026-05-06T02:30:38.24Z" }, + { url = "https://files.pythonhosted.org/packages/90/09/1f0bc2b584a51702407592bbccfe2b404187f6f5ee5b4b0c112a73e1a7ec/optree-0.19.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fff5fd89a9b333d91a05a7ca2e66c8e6632d0bdbc94c1725a341b77001f09511", size = 425490, upload-time = "2026-05-06T02:30:39.432Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f4/d8685b55323c1f42695c1ed647d6541ee9c289eb821abc6e0cb84b0e4f72/optree-0.19.1-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d4bac18638fa56efd2377cf8c43e17cd083aa566e69a31ce10f7fdaefd9676a3", size = 390552, upload-time = "2026-05-06T02:30:40.506Z" }, + { url = "https://files.pythonhosted.org/packages/07/84/ee12e234ddcf4fd4b7893ce03ec37f3c3edabdac911fd5384aa3f5c04c05/optree-0.19.1-cp310-cp310-win32.whl", hash = "sha256:ef2409d4efda1c5a6eb69f83ffff89fb04d5607fd056704552ec359fb865cd6c", size = 303117, upload-time = "2026-05-06T02:30:41.61Z" }, + { url = "https://files.pythonhosted.org/packages/91/b5/4e23965aacae04eb4cf42cd8108405a6628e645ee3ab759277e03063af0e/optree-0.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:01c88294235b118b7478b5e80d360e5f110977cdf79f84d61dae21c2eb1d4cdd", size = 327866, upload-time = "2026-05-06T02:30:42.664Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/4671a78193f96e86c1343fe04324091e163973d0058b292c10bc3387bb70/optree-0.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a496b864fe1fe0b5ea23d1ee3d1ef958d910704661808db2b2d2e16a0cfac96c", size = 414314, upload-time = "2026-05-06T02:30:43.812Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/7decea24656f416d61fa57b7113b1fbdbc042b7ab421399a84e1755676a1/optree-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1667e502e0eda9477925fb17c2ad879b199a2283ac98f18e6453692819b7811", size = 385006, upload-time = "2026-05-06T02:30:44.895Z" }, + { url = "https://files.pythonhosted.org/packages/af/2e/9d1bd2527481681c4399beeeabba11dca36b16ec814579f2e8cc6bc2af96/optree-0.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:42e367a9d81e57c31a23247094727987a2f64b708901233a42a24d44d24e93f6", size = 406124, upload-time = "2026-05-06T02:30:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/df/29/cdb40de6307809fa8e9452e4f9a65881a3140d01d9d589a07e9d054d8e1c/optree-0.19.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:96fad6c7b3a6fde3a0c8655fd003359cd247f8400749217502591a5ffc328699", size = 466772, upload-time = "2026-05-06T02:30:47.766Z" }, + { url = "https://files.pythonhosted.org/packages/cb/15/4645e1816e815a1306bbb7e3e2e6ba124f6dc325f8088a2db69301219a0c/optree-0.19.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3a900df0ffb9b8259961b337289754531a7e0a5de2f681e9c80866b6a7cb74e", size = 466203, upload-time = "2026-05-06T02:30:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d2/5758c76bdd7034b721d84c7f0fd911f3b39dcb489eeb27f674aaae8a5f5c/optree-0.19.1-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:27c8dc0f89ade9233aa7ed25ce15991da188e6950eb17cc0c313fc1f327c5b0b", size = 465030, upload-time = "2026-05-06T02:30:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/09/b9/f668bc51129c0fec7728ae8b43180417fe1c1fe99f71d302739f6cc50944/optree-0.19.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38f2e503fad50aff58cade85db448002d4adc72f4b3b50dcc7f3ef4bcd3b0173", size = 447141, upload-time = "2026-05-06T02:30:51.42Z" }, + { url = "https://files.pythonhosted.org/packages/a4/08/a7b8862e4465bf250c3ccc78db4d10b9a2cf90ce4db3681cbdf7eb076fb7/optree-0.19.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:5dc35cb31540ab6ed9850b0f8865ccd400994ebd51fcf0c156cc772073f43c04", size = 410016, upload-time = "2026-05-06T02:30:52.695Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/04b71a34cf5e663a1df029acceb5efc8a96c8dc4b0b6af6e98486638e913/optree-0.19.1-cp311-cp311-win32.whl", hash = "sha256:d32b1261be71211f77837e839e43a3e3e8fc57707091d2454d0a88590fb6abe8", size = 311810, upload-time = "2026-05-06T02:30:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/22/64/3cc7b08cb1c0f1949895f9490217ca8db6ced7f3bf75c65a5bf31c07bf1e/optree-0.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd28a527bb363a1d7d28e8b2fb62816ace6743418bb86e9c5f27ea6877dcdf6c", size = 337620, upload-time = "2026-05-06T02:30:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/85f4b05765287658529f09ede10461224161dcf0e29e6fce1ae488451cfe/optree-0.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:7853b58aa084e882ea078f390936bd92e46972eb8f9b5e654360b6480ca7283b", size = 349337, upload-time = "2026-05-06T02:30:56.647Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a7/cb5567029a608a296b0ca224025d03bba0365b41df19085b9b580191f6f2/optree-0.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:96e5c7c3b9144f08ae40c3d9848cfbcfa36b6bead0f8215ad071d5922ee6c4a5", size = 424023, upload-time = "2026-05-06T02:30:57.732Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a1/3651fb32fa8617108204aa4056d283af742020e0987d106f41402005d800/optree-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d9d198343e1e6ced18bef0cbff84091c1877964fc4a121df33f18840e073a01", size = 394782, upload-time = "2026-05-06T02:30:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1e/676470909aa64d7aba7c5edf83b171dc83b7af901d9ebb8e6d7512fe913a/optree-0.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a1202371d9fe3aa75f3e886b1f871aac4991a655aadb65e54f58a3ae9388ab2", size = 413157, upload-time = "2026-05-06T02:31:00.339Z" }, + { url = "https://files.pythonhosted.org/packages/f4/41/1a4c58f2af5742b9d9e21ea9e45c6c3c49463b5e2a0537e84ead1e9597ca/optree-0.19.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d41ccc4c20bfeae01d1d221c057a6d026e84e32229664952eddcdbe4b9b71417", size = 476923, upload-time = "2026-05-06T02:31:01.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/f62167bd9d6f6c948b191a0943923404678d47100f777f4a8fb37816e6f8/optree-0.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d934f240b109c6891dd06b2e30400b123b8a4b6ed31dcd0db2ae2378d30a6e8", size = 475385, upload-time = "2026-05-06T02:31:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/30/5e/5323c5fa3024fdd900bdd8f14621139ed844c2247bf1a26e7cf5c1116188/optree-0.19.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddeefb7ca799c09647e332ebc1a5f6c09888a5a0e51f2dff4ca55e65b42a8c14", size = 474406, upload-time = "2026-05-06T02:31:04.023Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/54e4c47e61a51504a5224c933722e0c8a69925aacec4c08175e9675aeb81/optree-0.19.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0ce49f64f804f7f35f2f9c2a21e3ba94c090199fccdcfd40e3ded4426c5c175", size = 457596, upload-time = "2026-05-06T02:31:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/bba07c0b769586c6bd54e81f1f734cad103dbe30abbadee940fe7d3e330e/optree-0.19.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e0f02600832ab8d0f6c934dcb5c339e17a36938d477641a45798e02625ebe107", size = 417900, upload-time = "2026-05-06T02:31:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8f/6ae994bb47f9394b33912a14593f9247737dd6c3303811550e5a3e918107/optree-0.19.1-cp312-cp312-win32.whl", hash = "sha256:f10d58c1a17e1b32f9d9b5e1b9d1ad964d99c1113d9df0b9f62f2fe7dde19909", size = 317302, upload-time = "2026-05-06T02:31:08.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/97/d7e3ec79dcdde81f785a0446acf75fea77723f5ca4b98556350d7877986f/optree-0.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:06f5c8a4cf356a1a276ce5cec1be44719ed260690f79c036d04b4d427e801258", size = 341362, upload-time = "2026-05-06T02:31:09.689Z" }, + { url = "https://files.pythonhosted.org/packages/33/97/813afb84a81fd8ae65444730907c05f0775fd6c79d3359c9e84bd3370445/optree-0.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:a33bd23fc5c67ecb9ff491b75fde10cd9b53f47f8a876de842090e8c7a2437e1", size = 351838, upload-time = "2026-05-06T02:31:11.086Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7b/0f2f3c9d55dda5127624daf68ff802ab624b739dd4b32aef505dac0c8e02/optree-0.19.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:f144cfd65fb17c6aa2c51818614eb009e6052d3d6ace91f7e570b1318cdcac4c", size = 929090, upload-time = "2026-05-06T02:31:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/670d260dfd0532d64272dd6f7edd540a09d7040c0342b6cc6cf773568ea4/optree-0.19.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:39a006735d2a0a68751a3bc33d670184fddcd86db63b0293e1e819739e8105e4", size = 391528, upload-time = "2026-05-06T02:31:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/96/46c15e80b0c97e2ba6aba11339008a37cabc5ccf55c31c6c60aecdb79638/optree-0.19.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d2cb43c36638f469f5d8f4cf638e914de90c62242d8bed29f1b4487e0346ab94", size = 398231, upload-time = "2026-05-06T02:31:15.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/39/9d7d22cdaeb9a40ace2485f91c5b7c5f3a7f688575e2621e436561211cc1/optree-0.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e70faa00ab69331f49f8337d45021bed09ae2265d1db72eea9d7817af2b73c64", size = 429852, upload-time = "2026-05-06T02:31:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/79/4c/1da9e8375e7b7fd9671dc5987682b042f6412c4d6fd9da03296403818d9f/optree-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1c5d21176b670407f4555aae40711668832599c4fb0627000c5ce3ed0d6e2dae", size = 398688, upload-time = "2026-05-06T02:31:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/cd2d178099618093f5a9fd1c9de80af2b428879922eae1e9f27f1002c8be/optree-0.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f658fa46305b2bdccdc5bb2cb07818aeaef88a1085499deda5be48a0a58d2971", size = 417560, upload-time = "2026-05-06T02:31:19.391Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/f22ff5632083b5032caa80208dd202f8e963ed4aac11afa0a0f6a307fd68/optree-0.19.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:e757079d44a00319447f43df5c51e55bf9b62d9f05eea0e2db5ff7c7ca5ec71d", size = 482937, upload-time = "2026-05-06T02:31:20.799Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d4/7499d28be8b11eb40668262d27802119fe7e6ec4cd8816b76a1acd7b08f5/optree-0.19.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9690c132822d9dee479cf7dff8cc52a67c8af42a4f7529d21f0f4f1d99e4c84e", size = 477864, upload-time = "2026-05-06T02:31:22.077Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6e/6c6fa6f1159ac68f4ee7666610127fb4c14d47a2fa7a0a48de3aecc24d4b/optree-0.19.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:544b70958dbd7e732bc6874e0180c609c9052115937d0ec28123bb49c1a574aa", size = 478319, upload-time = "2026-05-06T02:31:23.266Z" }, + { url = "https://files.pythonhosted.org/packages/68/b5/8a2427bbe4ee59e2ce26a14125728e3b48c7030c80984ba07d0e5d804d37/optree-0.19.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dde5b756946c1f1458aeab248a7a9b0c01bb06b5787de9f06d52ad38b745557", size = 462379, upload-time = "2026-05-06T02:31:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0c/a073eeaea4d4f68e02d5883ed8268746a296e6749e3c46e0124ca45f306c/optree-0.19.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:f1d7838e8b1b62258abd73a5911afad1153ed76822070558c3ba7e0bb5b44192", size = 423061, upload-time = "2026-05-06T02:31:25.652Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/637b151d071ca94aea0087322f470ce84c5828ef6b9c0de7dc7b4420a1cf/optree-0.19.1-cp313-cp313-win32.whl", hash = "sha256:9870d33ec50cca0c46c2b431cea24c6247457da15fd4ad66ccb8ab78145c1490", size = 317439, upload-time = "2026-05-06T02:31:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/49b8a8d9e94c57c6fa5008953f84a1c36a4119a3b90dcb7df745f1f05a00/optree-0.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:aa0845b725bcd0029e179cf9b4bc2cc016c7358e56fc7c0d2c43bf4d514c96cf", size = 343906, upload-time = "2026-05-06T02:31:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/1ae0a9685f5301f454f01d2490065b98df6956f90b1b2fd1cea9daa6d820/optree-0.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:6f0b1efc177bed6495f78d39d5aa495ccb31cc20bcf64bb1b806ca4c919f4049", size = 353146, upload-time = "2026-05-06T02:31:29.976Z" }, + { url = "https://files.pythonhosted.org/packages/9c/77/4c8108cbce2c8ae2aa4b6adc7874082882e32cf131cb64b3a4411f50dec4/optree-0.19.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b964bcdb5cfe367cdf56447e80ba5a49123098d8c4e8e68b41c20890eec6e58e", size = 469723, upload-time = "2026-05-06T02:31:31.425Z" }, + { url = "https://files.pythonhosted.org/packages/64/33/ce9b54646ed4ab5773a9dc59767dadfe3de8bb2e97a3ed19205b995a7a31/optree-0.19.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ccec0ee5a565eb5aa4fe30383016a358627ea23d968ec8ab28b1f2ce4ce3d8", size = 437071, upload-time = "2026-05-06T02:31:33.027Z" }, + { url = "https://files.pythonhosted.org/packages/79/55/04260128a726e3550b49467a65bff859452897144b68bae54b2f2e5c27f1/optree-0.19.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:672588408906051d3e9a99aca6c0af93c6e0b638137a701418088eaa0bb6c719", size = 433503, upload-time = "2026-05-06T02:31:34.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/99/6a4cc29389667efa089a0c476b7c36b7d0a66e10dd2d8c2d19c776977566/optree-0.19.1-cp313-cp313t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d16cef4d0555d49ce221d80249f1285a2d3faf932e451c3ce6cb8ccb6a846767", size = 496305, upload-time = "2026-05-06T02:31:35.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/506aa1a64abce69e2f4cec9cdac3da0cae207cf04c5e70e7f143bf8b29d8/optree-0.19.1-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc2db0b449baff53aa7e583306101de0ade5e5ae9e6fce78400eb2319bbd23dc", size = 492759, upload-time = "2026-05-06T02:31:37.265Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/2210de9a68722007fe007da3cae1a5971b92fc8113b5eecef66a04637959/optree-0.19.1-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:76b3e9e5d37e6b05ec82fff91758c8c0e27e159b35faea4b33d5eb975d720257", size = 495447, upload-time = "2026-05-06T02:31:38.505Z" }, + { url = "https://files.pythonhosted.org/packages/d9/61/40c3463e52914d552c66c760ae15e673137c4cc1d1d9f8da0d745656193a/optree-0.19.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03faa8e23fdaf3a18f9a1568c2c0eb0641a6aa05baf3a20639bd11fb34664700", size = 475564, upload-time = "2026-05-06T02:31:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/1603680fa924e68e5697c1229510c0645db0a9c633a12d1a9bfdbfc9cb74/optree-0.19.1-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:a9b9c7e9148ec470124dc4c1d1cd1485dbeb35973357b5911b181a79090426d2", size = 442414, upload-time = "2026-05-06T02:31:40.908Z" }, + { url = "https://files.pythonhosted.org/packages/a5/58/34820bab11f28ba6b03fe9e151880ad591b43f26648f058c94451fbdfc3a/optree-0.19.1-cp313-cp313t-win32.whl", hash = "sha256:ab8ad9803376d553a2958471b6bb6842b7e15888e19cc6aeb76da96c6afd948d", size = 348644, upload-time = "2026-05-06T02:31:42.038Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2b/0be3f8b9765f366e3e12d0590e9c6514de110d0c5b3b9002f49e56bf15b1/optree-0.19.1-cp313-cp313t-win_amd64.whl", hash = "sha256:afd4abeb2783b2367093287bc6268ac9af244b20c8d9b01696ccfe817483b66c", size = 382445, upload-time = "2026-05-06T02:31:43.166Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/8c0882cdd42e28a23c1998297c8ad1202194510cbba8b050251429c641c0/optree-0.19.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b9120510d3f951e268e417a3f64f335bc1c539e1e80bff2129ddc6fb60ac7b56", size = 388040, upload-time = "2026-05-06T02:31:44.661Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/4e16e26375c56c9e40760697af4e2b72f196c2099e96cc783b63dcc862a8/optree-0.19.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:e1951ddc870f67430310fd17393971c30510ee9fd290525b44c12afe25f3c307", size = 927808, upload-time = "2026-05-06T02:31:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/87/ff1c6bb6b79a5d0b70b83f7ae8b78811a406a749b3ae4478a2122a7afb66/optree-0.19.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ae9d42718ebf985cdad3182364b5cf829193b8fd2c6d993fbb4111d38e2bdf96", size = 390981, upload-time = "2026-05-06T02:31:47.38Z" }, + { url = "https://files.pythonhosted.org/packages/82/25/fc648710102960f87d18cd8fc8a24afe14a5ec7827c64dfb1340230c0794/optree-0.19.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:930268ebfdebca43a8808f6293910d6ade2fe7c84fa784692017d7120d285226", size = 397756, upload-time = "2026-05-06T02:31:48.76Z" }, + { url = "https://files.pythonhosted.org/packages/24/f6/a7bf5d75a6481038bbb61846d87d43124d63741385796ef7b37d326f46bd/optree-0.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b2757c5d922aab76cfc9b870c373fb35209c2094e3c912733b326c043e85a0c6", size = 427424, upload-time = "2026-05-06T02:31:49.838Z" }, + { url = "https://files.pythonhosted.org/packages/49/cc/14dd93887295859457e507fc46a847b68ae8f20c42b2fde4d8a749c94bbc/optree-0.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b17a7b70ff8bd406c2142914c5ab0a57f8bcfb9f52181f7012e32406bbdbfdda", size = 398242, upload-time = "2026-05-06T02:31:51.262Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/ac51aa118dd918761519fbc031865b1d6f850453e9a7ac0c3da21109c4f0/optree-0.19.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:987bba55366917d9829f45b5ee86499ecc87a30e9103072db9ab8d67f9958179", size = 419568, upload-time = "2026-05-06T02:31:52.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/41/25144e61f76278b9e0a5d4189c7083fe853164c5f7328a1f5aac43d964c2/optree-0.19.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d3bba2af7a5fce0c25e99024688e68dfe9be41e3d6e92720febbefdc879fba38", size = 482797, upload-time = "2026-05-06T02:31:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/2c76c7ce937323988770c41126e0e380bcb73a816f68a767f23b5c33aced/optree-0.19.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dae6c247cc8751bd2f167951468769f5c98f8cfdae31c0db0f2eb4145a6ec560", size = 479794, upload-time = "2026-05-06T02:31:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ca/bd9553f94bec0bc7860f10ae177c14ca265ab19ddb463122be22fa335ee8/optree-0.19.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17a986fd91ccdc18bb7b587ca1f508c1761580a93517e6db33a13b22e46acb9b", size = 481084, upload-time = "2026-05-06T02:31:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1a/4834b1f2fb1847412353d7342eb7a1d001a4f3bd9d24155e057135a4aa44/optree-0.19.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d0e1493429ae1d1a5e34855774ee604c974a8f76656bd0e562cdbf9466c9b1f", size = 462955, upload-time = "2026-05-06T02:31:57.829Z" }, + { url = "https://files.pythonhosted.org/packages/f4/88/598fb91c06fee3d8b08568779b011225dc2b66140927bd0b2b2d9b40a566/optree-0.19.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:f61a01ed9991193ed6f3db8e956ede05218190a32ca2ddfb71cfc40c8daba1d5", size = 423754, upload-time = "2026-05-06T02:31:59.291Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/83c64ecadc686e08310fc9c20bc0bbe6453e89b69257e08887818dac7886/optree-0.19.1-cp314-cp314-win32.whl", hash = "sha256:b0c920579bddc3b18a0e051850f017618e24efcc19ba83dcd415cf74db5fd904", size = 325214, upload-time = "2026-05-06T02:32:00.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/c3/4f2f318b98465376bbb7a06a33da553c688b3ed39dafbb8307f824eef74a/optree-0.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:50d77b91a8cd01adf422472b7edf39fc445b0268816176a868a385d28f8367c2", size = 351654, upload-time = "2026-05-06T02:32:01.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ab/55d7508e87055c730fe7207cfd0c45183a07ddf1f91d9e73d017a7f8c1f4/optree-0.19.1-cp314-cp314-win_arm64.whl", hash = "sha256:c682ab6711b7a623503711fa661a2bba7886e1c21dc06c3b7febba101b458051", size = 361610, upload-time = "2026-05-06T02:32:03.003Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/4f7facd482d56079b7adb8ce3fede19f41629bc0463e8ee25907f1dba36c/optree-0.19.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:068edb89fadd94f6f57fdb51f4ad2c764b5a0bfd00903c55ffe433c2863a8037", size = 469130, upload-time = "2026-05-06T02:32:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/92/60/f7539012aa8a7488c1e34f66b76eadc384c3152dd9800973f1b5fe045dfd/optree-0.19.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a609c90e4f64e4f3e2b5b3cc022210314834737e0e61a745485e33b33eae773b", size = 437286, upload-time = "2026-05-06T02:32:05.527Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3f/a5f8fb3ec3840f885de52d7a793ba57ace17990e3a9b3797218425ffe842/optree-0.19.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfae64c4c371640a4b3e2a9e3e6aa3a3e8cdf2da5247a88fef5b632614b948a6", size = 431954, upload-time = "2026-05-06T02:32:06.83Z" }, + { url = "https://files.pythonhosted.org/packages/68/dc/6d0ef14bc82bd54046c1a066d25fa6854123a6b29fd691f1f95dec3ab45f/optree-0.19.1-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:470742544ff2d4b63843023f38dcfb83e82c3a9877c783dee0e69cbb974de6d1", size = 494631, upload-time = "2026-05-06T02:32:08.038Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9a/9e183c610c414cba581f9afda7610589d89cae229d627b14f8480425d975/optree-0.19.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1a74e0656ccef45b1fec07b9d964ce97f3def8bab73711f56175076c4259884f", size = 491786, upload-time = "2026-05-06T02:32:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/266b9de8eb5b16bfe7010c90c55840517d5d61ee6e0ca64901440296d97a/optree-0.19.1-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55841132ba8a34dbbd85e0c2cf990602384eea0e4638df986cd3266482f4a17", size = 490876, upload-time = "2026-05-06T02:32:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/42a8ca6277ef93d47ab0986e30a25134206afe0c6e6c3425c8736b2677ba/optree-0.19.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5f8383952f18d5a4ec6b248d8ae6fe27012434ad9750aa33a821ad4846da5af", size = 475079, upload-time = "2026-05-06T02:32:12.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/91/e363f4adda292f891ca0cf5748010fea955737bdf494cc11d4c3bcda6935/optree-0.19.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:de8acbed5965beae6f6b0456fcb8d1afaea1fe300810739e88645e22138849bc", size = 440119, upload-time = "2026-05-06T02:32:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/489d22ef3cadb2f5f3bbd6e6099d17b5a521ff533e086f78f005c3358017/optree-0.19.1-cp314-cp314t-win32.whl", hash = "sha256:312048e69dc88de26915674f961bf38980a765a6b48ead2f1672858a39402c41", size = 357465, upload-time = "2026-05-06T02:32:15.424Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/7f48b7034ff75d2eb3e94e2196709ddbf762798fb621f9508899fa66b44e/optree-0.19.1-cp314-cp314t-win_amd64.whl", hash = "sha256:60e9345405d7b06cafdf1b1dd2e2261ceddddce10f35729240f90e2bab845a0b", size = 397783, upload-time = "2026-05-06T02:32:16.853Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/6d6f93416c66820cb8753e65b5ff43c47480af9c4911bd2b8406ff0f7f27/optree-0.19.1-cp314-cp314t-win_arm64.whl", hash = "sha256:4e103e212d1e8fe0399ed076eff80a905fb14929729bbd994d3660110a27a252", size = 396064, upload-time = "2026-05-06T02:32:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d4/ffeedc86f8b91e5c17994f38bd1f7aa2e20f9b70a6d3ae906af16414626c/optree-0.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3f4f1c276fa06227cdaf58349d22a3231b3dd3d47de1f90a86222ebf831fc397", size = 417543, upload-time = "2026-05-06T02:32:32.592Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/80fb1b289940e34858cb89f05bc7ce23d6d1272886c2f78bc7e3ab1a306b/optree-0.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:77d93eafbd0046c7350bc592ab8e3814abbd39a6d716b5b1e5d652cc486f445c", size = 390184, upload-time = "2026-05-06T02:32:34.273Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/f31784a7a2dcc0c1f84b691afc552ea5b26db5f56657692a12954a828db4/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3507ae5db5827eef3da42d04c5a41df649cedc2e42d5d39dc0f869d36915a00b", size = 409025, upload-time = "2026-05-06T02:32:35.817Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a5/647b93eb16244cc7f6dfccc025ac495245e306ff4cb8f9ad15718219141a/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:732c4581fb666869b8b391ec4ca13d2729795f9abe72b5aec2e582bcbea1975d", size = 449514, upload-time = "2026-05-06T02:32:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/d251c9338771ef0f9db8e538bd77810100c495734b57494464c7e223f0d0/optree-0.19.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e12ee3776a16f6feaa8263b92469ad546b870af71d50602745855d8449219221", size = 341586, upload-time = "2026-05-06T02:32:38.308Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[package.optional-dependencies] +hdf5 = [ + { name = "tables", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tables", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +performance = [ + { name = "bottleneck" }, + { name = "numba" }, + { name = "numexpr" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "pims" +version = "0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "slicerator" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz", hash = "sha256:55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b", size = 87779, upload-time = "2024-06-10T19:20:42.842Z" } + +[[package]] +name = "pint" +version = "0.24.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "flexcache", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flexparser", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "platformdirs", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/bb/52b15ddf7b7706ed591134a895dbf6e41c8348171fb635e655e0a4bbb0ea/pint-0.24.4.tar.gz", hash = "sha256:35275439b574837a6cd3020a5a4a73645eb125ce4152a73a2f126bf164b91b80", size = 342225, upload-time = "2024-11-07T16:29:46.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/16/bd2f5904557265882108dc2e04f18abc05ab0c2b7082ae9430091daf1d5c/Pint-0.24.4-py3-none-any.whl", hash = "sha256:aa54926c8772159fcf65f82cc0d34de6768c151b32ad1deb0331291c38fe7659", size = 302029, upload-time = "2024-11-07T16:29:43.976Z" }, +] + +[[package]] +name = "pint" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "flexcache", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flexparser", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "platformdirs", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "4.25.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/d08c41a8c004e1d437ef467e7c4f9c3295cd784eba48ed5d1d01f94b1dad/protobuf-4.25.9.tar.gz", hash = "sha256:b0dc7e7c68de8b1ce831dacb12fb407e838edbb8b6cc0dc3a2a6b4cbf6de9cff", size = 381040, upload-time = "2026-03-25T23:09:36.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/e9/59435bd04bdd46cb38c42a336b22f9843e8e586ff83c35a5423f8b14704e/protobuf-4.25.9-cp310-abi3-win32.whl", hash = "sha256:bde396f568b0b46fc8fbfe9f02facf25b6755b2578a3b8ac61e74b9d69499e03", size = 392879, upload-time = "2026-03-25T23:09:21.32Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/42a5c7f1001783d2b5bfcecde10127f09010f78982c86ae409122ce3ece6/protobuf-4.25.9-cp310-abi3-win_amd64.whl", hash = "sha256:3683c05154252206f7cb2d371626514b3708199d9bcf683b503dabf3a2e38e06", size = 413900, upload-time = "2026-03-25T23:09:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/0074a0a9eb01f3d1c4648ca5e81b22090c811b210b61df9018ac6d6c5cda/protobuf-4.25.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:9560813560e6ee72c11ca8873878bdb7ee003c96a57ebb013245fe84e2540904", size = 394826, upload-time = "2026-03-25T23:09:25.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/b2dba856f64c36b2a06c67be1472de98cca07a2322d0f0cbf03279a40e5b/protobuf-4.25.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:999146ef02e7fa6a692477badd1528bcd7268df211852a3df2d834ba2b480791", size = 294191, upload-time = "2026-03-25T23:09:26.613Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/53f18822017b8bda6bd8bb4e02048e911fdc79a3dafdc83ab994fe922a84/protobuf-4.25.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:438c636de8fb706a0de94a12a268ef1ae8f5ba5ae655a7671fcda5968ba3c9be", size = 295178, upload-time = "2026-03-25T23:09:27.839Z" }, + { url = "https://files.pythonhosted.org/packages/16/28/d5065b212685875d3924bcdb3201cbf467cb4d58a18aa19a8dfd99ea80a9/protobuf-4.25.9-py3-none-any.whl", hash = "sha256:d49b615e7c935194ac161f0965699ac84df6112c378e05ec53da65d2e4cbb6d4", size = 156822, upload-time = "2026-03-25T23:09:34.957Z" }, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psygnal" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/79/20c3e23e75272e9ddf018097cf872ab088bccba978888472656629efa4a3/psygnal-0.15.1.tar.gz", hash = "sha256:f64f62dee2306fc1c22050a59b6c6cdad126e04b0cf50e393ff858a1da719096", size = 123147, upload-time = "2026-01-04T16:38:41.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/44/ab13cb6147d010258826a43e574ad94599af0de29df13795fff9efee656c/psygnal-0.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ee55e3997f796fd84d4fdbd829bb1b19d323e087c43d072744604a3016c8851", size = 587322, upload-time = "2026-01-04T16:38:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/68c042a607ca613e9450dfee99cc5c2a4d10d95392fb1de2ba932dd0a605/psygnal-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:912bcf110bfe7b4aa121d24987b6a58afb967ff090a049dad136eaf3cbcc7bea", size = 576207, upload-time = "2026-01-04T16:38:06.183Z" }, + { url = "https://files.pythonhosted.org/packages/4b/86/123c7b169ad32994a0cd801cd1f11c1a2be84555807e9c8a8a4682c67a9f/psygnal-0.15.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2e860c11fe075fd80c93a24081c577ef7ec5c9da41f0e75990aa4cccf3f79cf", size = 864261, upload-time = "2026-01-04T16:38:07.895Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/886cec7bec2f27fe453cfa32bfcaac08a83aab2a04895af68f93e1c493b8/psygnal-0.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5b8bebcf99699ef50b6ef572868a490f6d191dc4466e5bd9818ca27e17cd581", size = 872582, upload-time = "2026-01-04T16:38:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/21/a3/da972a05568ee8a9dc6c6567bee2c0cc5af8c681baebcb9fdbbf3cceb4f7/psygnal-0.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:06e0a90490e1205620d97ac52fbbe3282a22b126a26d02b3e1196bb46de16c7a", size = 411043, upload-time = "2026-01-04T16:38:11.588Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a7/69495410025cc4298765545ce3b8c635cd4c8d3a362b7fbbc15b80e9fc8f/psygnal-0.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1adc41515f648696990964433f1e25d8dfd306813a3645366c85e01986ba57a0", size = 581002, upload-time = "2026-01-04T16:38:12.753Z" }, + { url = "https://files.pythonhosted.org/packages/75/1f/19a8126ccf3cd3974ba5d08a435a049b666961d90f5848ba83599d7a29de/psygnal-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:38ff18455b2ac73d4e8eea82ef298ce904b52e4dfdc603a24380c9c440e37519", size = 567775, upload-time = "2026-01-04T16:38:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/b1348880d603edb82128a721397a1ddcf3dfcf5384fe5689db6e471118ae/psygnal-0.15.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c923c322eeefb1140886927cfe7bda7c32341087e290e812b9c69a624ab72d54", size = 855961, upload-time = "2026-01-04T16:38:15.612Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/3da2d6f3583bd1a849f7faa2fd3492b14bfda05012519ceaea5992658af0/psygnal-0.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2714ddaa41ea3134c0ee91cebd5fb11a88f254ea1d5948806ab0ad5f8be603d5", size = 862721, upload-time = "2026-01-04T16:38:17.059Z" }, + { url = "https://files.pythonhosted.org/packages/4d/14/6fc7e97fdecf7e8c5c105684bab784920312a3259800d8b53e3cf8783f42/psygnal-0.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:877516056a5a383427a647fff2fad5179eaa3e12de2c083c273e748435414aef", size = 415696, upload-time = "2026-01-04T16:38:18.355Z" }, + { url = "https://files.pythonhosted.org/packages/76/65/b7bbca96bc477aa9ac2264e5907b2f4ccfcd1319f776dd1f35eec06cc2f4/psygnal-0.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d56f0f35eaf4a21f660de76885222faf9e8c7112454528d3394d464f3d4d1a3", size = 598340, upload-time = "2026-01-04T16:38:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/56577465a1b42a5e6780bb5fab53fb68f8bfd72f0131ed397576529af724/psygnal-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0febcf757a1323d9b8bd75735ee3569213d8110012a7bf0f478e85c5ab459fc6", size = 575311, upload-time = "2026-01-04T16:38:21.137Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/f642ac08104049383076f83480ed412c9626e068769a1c34873c595bec0e/psygnal-0.15.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5e4837dfbfa4974dabe0795e32be9aadcd87603adf734738ce1114f72238a05", size = 889770, upload-time = "2026-01-04T16:38:22.629Z" }, + { url = "https://files.pythonhosted.org/packages/de/43/e571fa40b72780abed080ef829e5ad98017b6fe48d28c15a2404e006b676/psygnal-0.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07b4c4e03bbf4e8cad7e25f4fbc1ba9575fb9c3d14991bc7edfeb8b09c8d6d54", size = 881105, upload-time = "2026-01-04T16:38:23.896Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/ef3ab825eb08eaecbbceeeb56383694fe64ce399dbfd1d0767bb85688785/psygnal-0.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:4f0ce91b9c18e92281bf2c3fc4bb4e808d90f0b023d0a37b302d354188520338", size = 418969, upload-time = "2026-01-04T16:38:25.731Z" }, + { url = "https://files.pythonhosted.org/packages/46/21/5a142165d27063abf5921807d3c3d973f5d44ab414a13b210839a43ead4d/psygnal-0.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2087aadc9404f007f79c2899e329932869e362c50de58b90631c5f49b4768cc5", size = 596768, upload-time = "2026-01-04T16:38:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/25/c1712931d61c118691e73daf29ef708c679ea9ba187c797dd5deee360411/psygnal-0.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f3bf68ca42569dfdce20c6cf915d34b78b9e3ddddacb9f78728224fda6946b4", size = 574808, upload-time = "2026-01-04T16:38:28.779Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4f/3593e5adb88a188c798604aed95fbc1479f30230e7f51e8f2c770e6a3832/psygnal-0.15.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e9fca977f5335deea39aed22e31d9795983e4f243e59a7d3c4105793adb7693d", size = 885616, upload-time = "2026-01-04T16:38:30.081Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/14779ed4c3a1d71fa1a9a87ecfb184ad3335dd64681067f77c1c47b14ae9/psygnal-0.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c85b7d05b92ccbec47c75ab8a5545eda462e81a492c82424aba5ab81a3ad89d", size = 876516, upload-time = "2026-01-04T16:38:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bc/4f771e3cdcde4db4023dbf36d6f0aab44e02b9de719353c22954b655e2ff/psygnal-0.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac0e693b29e0a429e97315a52313321855bef6140e9975b7ae78b4d93c8fbb42", size = 419172, upload-time = "2026-01-04T16:38:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2e/975bd61727578d88df62797f78390965ca7905780cf01eb59cb095a13638/psygnal-0.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:803fc33c4280c822c6f4b22e6c3ea7c4483e190f3cc69e69350098b3799476f3", size = 595706, upload-time = "2026-01-04T16:38:34.139Z" }, + { url = "https://files.pythonhosted.org/packages/b8/55/e487f1d91497eb75e86c3fdfef69a21b1cab24d023383dd7648b08797d6a/psygnal-0.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4f53b4b83355b0a785b745987fd04e59bbf169a9028ed81a68ca7e05fb76d458", size = 575133, upload-time = "2026-01-04T16:38:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2f/f286355accd0e68d3eef52e63c8b9ab6ba33ec3107177719a036b3319657/psygnal-0.15.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcbca12190f5aa65c1f8fb04a81fa6f4463c5f5dde25cd74c3a56ceff6f37b02", size = 889565, upload-time = "2026-01-04T16:38:37.003Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dc/40c6026c88d7f9220ecc913afe0501045a512c9b82f9b7e036bb089dc287/psygnal-0.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ac399566852fe4354ce26a1acbe12319232e8c2b615fe5ad1e114c547095cf6", size = 880863, upload-time = "2026-01-04T16:38:38.381Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/b4f45ec3057c473b5622fc002b3a636a698c34d3a0917a064ff5247f1984/psygnal-0.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:d3a03055f331ce91d44581c71edb79938ccc133a94af2ce7ad3a18fa57ac7be5", size = 423654, upload-time = "2026-01-04T16:38:39.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/49/7742544684bee728ec123515d2694cee859aa2a705951a461230b00f18cc/psygnal-0.15.1-py3-none-any.whl", hash = "sha256:4221140e633e45b076953c64bcb9b41a744833527f9a037c1ca98bc270798cbf", size = 90638, upload-time = "2026-01-04T16:38:40.841Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, + { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, + { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, + { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybtex" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "latexcodec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f5/f30da9c93f0fa6d619332b2f69597219b625f35780473a05164a9981fd9a/pybtex-0.26.1.tar.gz", hash = "sha256:2e5543bea424e60e9e42eef70bff597be48649d8f68ba061a7a092b2477d5464", size = 692991, upload-time = "2026-04-03T13:05:39.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/f6/775eb92e865b28cdb4ad1f2bed7a5446197516f76b58a950faa3be3fd08d/pybtex-0.26.1-py3-none-any.whl", hash = "sha256:e26c0412cc54f5f21b2a6d9d175762a2d2af9ccf3a8f651cdb89ec035db77aa1", size = 126134, upload-time = "2026-04-03T13:05:40.623Z" }, +] + +[[package]] +name = "pybtex-docutils" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9", size = 6385, upload-time = "2023-08-22T06:43:20.513Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + +[[package]] +name = "pyconify" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/7f/94d424dc756a6287271cf40cf1b2a44c10e3f137bf3246a2b4a7416ca3d3/pyconify-0.2.1.tar.gz", hash = "sha256:8dd53757d9fbed41711434460932b2b5dbc25da720cd9f9a44af0187b2dfc07d", size = 22478, upload-time = "2025-02-06T13:20:53.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/40/50dd2e8bfec81676e4619903bd452c10dc0d8efac1533e79e67cc76759b5/pyconify-0.2.1-py3-none-any.whl", hash = "sha256:d3b53eee1f8a2d60c1d135610f42e789774dbe71c6d8af68af0a21d3b3ec9eb7", size = 19459, upload-time = "2025-02-06T13:20:51.613Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-compat" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/7e/43400b6e0800065a982efcfa3e87c8f8d247d60ea75ca1a9d01702e050f8/pydantic_compat-0.1.2.tar.gz", hash = "sha256:c5c5bca39ca2d22cad00c02898e400e1920e5127649a8e860637f15566739373", size = 12838, upload-time = "2023-10-24T23:25:09.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/65/2edf586ff7b3dfc520977c6529c9b718c86ef8459ece088f1ef1f74bf1d4/pydantic_compat-0.1.2-py3-none-any.whl", hash = "sha256:37a4df48565a35aedc947f0fde5edbdff270a30836d995923287292bb59d5677", size = 13092, upload-time = "2023-10-24T23:25:08.155Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "accessible-pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "babel", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "beautifulsoup4", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, +] + +[[package]] +name = "pydata-sphinx-theme" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "accessible-pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "babel", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "beautifulsoup4", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pygments", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/0d/8ba33fa83a7dcde13eb3c1c2a0c1cc29950a048bfed6d9b0d8b6bd710b4c/pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde", size = 6723264, upload-time = "2024-12-17T10:53:35.645Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, +] + +[[package]] +name = "pyopengl" +version = "3.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pyside6" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/15/1fa71e44d3b345b9eca82d38cf7e6d7505168b978fe98b3610c0e25d8c0e/pyside6-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:6fd9fbbc14e2a5707df611bfacbf3f71283c637b8a29b28708801eeb28bfcb69", size = 554720, upload-time = "2025-09-30T12:02:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/1e9841cc46196c55ac3eac0b8e08044a88cc70c8cc29e9dc1e33b2ced2b7/pyside6-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6485aebec8eba4e55d1ec1cebe68ca1413589880cc8ccd8a49acae852ec6cfb3", size = 554849, upload-time = "2025-09-30T12:02:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/6aeb7124a5c08ac537776d7d0be1011cf22ccfc6f95cf901fe1eb1a16e91/pyside6-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:8f4ff61b24a64153373b68a96339bd765fc010d02c4d98d0f6dba2a6c9686e11", size = 554846, upload-time = "2025-09-30T12:02:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/29/4eb0ab29ee7d60da4c5fefdb514c22ae3c91c3f855f46608cfbe23816518/pyside6-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:71b41b9ebd1c044c3777f2b32278d3919f07bda4f15c504bc165b643bc3cec01", size = 561082, upload-time = "2025-09-30T12:02:12.411Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c2/b936d50974a14846ccb883cbce0034ec431e1013af7f2aa29d3746a1565f/pyside6-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:33fb70addd02e1adaa45573485c431bca43cf12bb7b2596535b824ff169138ce", size = 545910, upload-time = "2025-09-30T12:02:13.746Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/56/714c55e4514ec6603be8126355baf416e507557a73e7fc76870e6f5c20b9/pyside6_addons-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:189c9a9a2fdaffa95e91731f5c0afdc47ba231f5f683d3f8977b22c233749ba4", size = 316906068, upload-time = "2025-09-30T12:03:28.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/fe/d5c67665f866b8859d02aa1a859f101a1b2fd348cb61746a3e16fd98fb20/pyside6_addons-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:68932327e1c33d729d79b2b94242f97b77601efe0427e757cd3fd588939ea479", size = 167175405, upload-time = "2025-09-30T12:04:11.793Z" }, + { url = "https://files.pythonhosted.org/packages/66/f2/66da0d8ba8e4eb934d2bb042f2199664d2366c121844e36376f724b53fd9/pyside6_addons-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:61f4f4859bd1711eea2202fe364a701597140ed4f3900ddf1f90d91ae631fdf9", size = 163102125, upload-time = "2025-09-30T12:04:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fb/849599666942ce35e9f250bce1abc7b1248eccdcbe8b3fe697a0f98a1326/pyside6_addons-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:0aece2a81ccf16ef9b750b09601b6876aa116bbc700e848ce82df42906f04c5c", size = 160681555, upload-time = "2025-09-30T12:05:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2d/e71face0149519d6ded4e77b5c5e266c094ce5ef8c5c96c2af2f0afadf3c/pyside6_addons-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:3b8749c9c2b297f53c980192e89ce38ac59019f290a2669fe1abb3128f9a09d9", size = 33759045, upload-time = "2025-09-30T12:05:48.373Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/a9/51549844900837c70e5c89cbd840956c2533732ef4f05c2b656e35ca8182/pyside6_essentials-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ad3664ff0ced9f92ed7872e512c86328894d29f262e6c3400400232a36dda357", size = 134586559, upload-time = "2025-09-30T12:06:22.681Z" }, + { url = "https://files.pythonhosted.org/packages/85/e8/9396cf11a60f80175bb3c5c1d498d84e87b7af653ab4ea001acf821a3981/pyside6_essentials-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c70d5544e892b201a677b615156fab6a0fef865e7fc287f55a0eae00a682e83f", size = 97495307, upload-time = "2025-09-30T12:06:49.213Z" }, + { url = "https://files.pythonhosted.org/packages/57/a5/0187b2845a6fa534217988f9351c0b85ba1c04e8fd31f1e3c13ba1c4386e/pyside6_essentials-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e22e517c592657e291c0cbe7d04078ab415cb225188d7e895f8f7adcdba755d2", size = 95199568, upload-time = "2025-09-30T12:07:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/52/b558c79bc32310361a37d1e894c2251a3557daec380be5f6d2a9223e8ef3/pyside6_essentials-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:21f98077c135864473089e59a6a0bd828e64c6644b3dd7267b102da4a2ee8f21", size = 73593457, upload-time = "2025-09-30T12:07:33.98Z" }, + { url = "https://files.pythonhosted.org/packages/d2/22/4ec828f6360e6e9bcd6fde2dec20fa4865fe1a77cb6bac6812f7d0aa55b3/pyside6_essentials-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:2e34081933e005686d79265cc04370a28fea3844ab63d432e493adcd4465070c", size = 54301788, upload-time = "2025-09-30T12:07:49.504Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-qt" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "qdarkstyle" +version = "3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/1c/00ca31b13727ade22d1b42b61dc86056493a72f01912082a61cb34e5abf6/QDarkStyle-3.1.tar.gz", hash = "sha256:600584d625343e0ddd128de08393d3c35637786a49827f174d29aa7caa8279c1", size = 698602, upload-time = "2022-05-30T15:56:52.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/59/01f454d0eacb6670c77add68611b7a572455ae69ba902d270ed761869f87/QDarkStyle-3.1-py2.py3-none-any.whl", hash = "sha256:679a38fcd040de9fac8b8cae483310302fdb12c8d912845249c41dc54974a9b2", size = 870167, upload-time = "2022-05-30T15:56:49.502Z" }, +] + +[[package]] +name = "qtconsole" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython-pygments-lexers" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "qtpy" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/28/4070eb0bacb99bc00bf60173fda25fb6a559d6600040035ba96c196d3647/qtconsole-5.7.2.tar.gz", hash = "sha256:27b485b9161925924c1d8e78e66bb342e6e3bc49bf675d0a67b49bad9c291521", size = 436661, upload-time = "2026-03-25T02:24:38.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/60/aba9f3c3f1f48c7fbdf03bed45b576a916cd09c08242939b5294b85e37b4/qtconsole-5.7.2-py3-none-any.whl", hash = "sha256:e1d1f6a792123363626e643a7a4ee561217773571043992693fba7eccfa89f95", size = 125883, upload-time = "2026-03-25T02:24:36.95Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +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.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lazy-loader", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[package.optional-dependencies] +data = [ + { name = "pooch", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lazy-loader", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[package.optional-dependencies] +data = [ + { name = "pooch", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/a2/2e6c090db384cc515069f4f85542bd5baf6786852073020ea73d4a76d3ea/sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978", size = 452946, upload-time = "2026-05-13T13:34:52.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" }, + { url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.9.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/ed/eac552326349629f8c3a89b57a754a860d9665aea78c172777395905d36b/shiboken6-6.9.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:e9b240828790b8e21a50e66449a5aa8b99f9b8a538c80c1a325fa04f8364985e", size = 400999, upload-time = "2025-09-30T12:00:08.123Z" }, + { url = "https://files.pythonhosted.org/packages/be/82/c1c6932f9849bc5e75c93c38a29419505a6e3e0037261e28f3e7ecbf2751/shiboken6-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f3f5337a3a8fc660ba1462265bd9a2bdda9588f8d90fbc3d5ac4ce3134c11e59", size = 204927, upload-time = "2025-09-30T12:00:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fc/026f4c8660494e513fd8c6d95d4d694d490795c6880f1fd23ec996a83e13/shiboken6-6.9.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:f66e82510e3a3170a42e3ef865329559b69811ce83102ad1ee57920319427c10", size = 200684, upload-time = "2025-09-30T12:00:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/f6bb3fc89623dd01ac67cd6be357e585ea5081536b275008b6c8d6fa7c28/shiboken6-6.9.3-cp39-abi3-win_amd64.whl", hash = "sha256:f1498176d2d5bcade7d662e1fc5143980fb008b6c78b966289d29a4ae86cd0c6", size = 1166242, upload-time = "2025-09-30T12:00:13.447Z" }, + { url = "https://files.pythonhosted.org/packages/c1/66/097a5f80ed1058071466c8c4fd72f94886851f6e03028f980ef0c42a7b54/shiboken6-6.9.3-cp39-abi3-win_arm64.whl", hash = "sha256:0c87bcfd483a1980794e03cf5ec6b2061691a1b075f92b78edb0d13847139ecf", size = 1727949, upload-time = "2025-09-30T12:00:15.23Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "slicerator" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/52/f38586b82b2935f8b59a09b0a79c545a22ed062e728c9418bafeb51f61e0/slicerator-1.1.0.tar.gz", hash = "sha256:44010a7f5cd87680c07213b5cabe81d1fb71252962943e5373ee7d14605d6046", size = 38283, upload-time = "2022-04-07T18:54:08.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/ae/fa6cd331b364ad2bbc31652d025f5747d89cbb75576733dfdf8efe3e4d62/slicerator-1.1.0-py3-none-any.whl", hash = "sha256:167668d48c6d3a5ba0bd3d54b2688e81ee267dc20aef299e547d711e6f3c441a", size = 10274, upload-time = "2022-04-07T18:54:07.029Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx-book-theme" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, +] + +[[package]] +name = "sphinx-book-theme" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl", hash = "sha256:709605d308e1991c5ef0cf19c481dbe9084b62852e317fafab74382a0ee7ccfa", size = 455936, upload-time = "2026-03-09T23:20:28.788Z" }, +] + +[[package]] +name = "sphinx-comments" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/75/5bbf29e83eaf79843180cf424d0d550bda14a1792ca51dcf79daa065ba93/sphinx-comments-0.0.3.tar.gz", hash = "sha256:00170afff27019fad08e421da1ae49c681831fb2759786f07c826e89ac94cf21", size = 7960, upload-time = "2020-08-12T00:07:31.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl", hash = "sha256:1e879b4e9bfa641467f83e3441ac4629225fc57c29995177d043252530c21d00", size = 4591, upload-time = "2020-08-12T00:07:30.297Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "sphinx", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "sphinx", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + +[[package]] +name = "sphinx-external-toc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, + { name = "sphinx" }, + { name = "sphinx-multitoc-numbering" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/85bcd2f1c142e580a1394c18920506d9399b8e8e97e4899bbee9c74a896e/sphinx_external_toc-1.1.0.tar.gz", hash = "sha256:f81833865006f6b4a9b2550a2474a6e3d7e7f2cb23ba23309260577ea65552f6", size = 37194, upload-time = "2026-01-16T13:15:59.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/80/1704c9179012e289dee2178354e385277ea51f4fa827c4bf7e36c77b0f4b/sphinx_external_toc-1.1.0-py3-none-any.whl", hash = "sha256:26c390b8d85aa641366fed2d3674910ec6820f48b91027affef485a2655ad7d0", size = 30609, upload-time = "2026-01-16T13:15:57.926Z" }, +] + +[[package]] +name = "sphinx-jupyterbook-latex" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/29/18a1fc30e9315e72f068637079169525069a7c0b2fbe51cf689af0576214/sphinx_jupyterbook_latex-1.0.0.tar.gz", hash = "sha256:f54c6674c13f1616f9a93443e98b9b5353f9fdda8e39b6ec552ccf0b3e5ffb62", size = 11945, upload-time = "2023-12-11T15:37:25.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl", hash = "sha256:e0cd3e9e1c5af69136434e21a533343fdf013475c410a414d5b7b4922b4f3891", size = 13319, upload-time = "2023-12-11T15:37:23.25Z" }, +] + +[[package]] +name = "sphinx-multitoc-numbering" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/1e/577bae038372885ebc34bd8c0f290295785a0250cac6528eb6d50e4b92d5/sphinx-multitoc-numbering-0.1.3.tar.gz", hash = "sha256:c9607671ac511236fa5d61a7491c1031e700e8d498c9d2418e6c61d1251209ae", size = 4542, upload-time = "2021-03-15T12:01:43.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl", hash = "sha256:33d2e707a9b2b8ad636b3d4302e658a008025106fe0474046c651144c26d8514", size = 4616, upload-time = "2021-03-15T12:01:42.419Z" }, +] + +[[package]] +name = "sphinx-thebe" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/fd/926ba4af1eb2708b1ac0fa4376e4bfb11d9a32b2a00e3614137a569c1ddf/sphinx_thebe-0.3.1.tar.gz", hash = "sha256:576047f45560e82f64aa5f15200b1eb094dcfe1c5b8f531a8a65bd208e25a493", size = 20789, upload-time = "2024-02-07T13:31:57.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl", hash = "sha256:e7e7edee9f0d601c76bc70156c471e114939484b111dd8e74fe47ac88baffc52", size = 9030, upload-time = "2024-02-07T13:31:55.286Z" }, +] + +[[package]] +name = "sphinx-togglebutton" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "setuptools" }, + { name = "sphinx" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/be/169a0b0a8ad9588e8697c85e1d489aaaca7416073c2fc0267c360af5aae9/sphinx_togglebutton-0.4.5.tar.gz", hash = "sha256:c870dfbd3bc6e119b50ff9a37a64f8991902269e856728931c7d89877e8d4b3d", size = 18101, upload-time = "2026-03-27T13:50:41.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl", hash = "sha256:74eac6d2426110c3e1e6f989a98e07d7823141a335df1ad8a9d637bdf6a7af62", size = 44907, upload-time = "2026-03-27T13:50:40.94Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-bibtex" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pybtex" }, + { name = "pybtex-docutils" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/6a/8e0b2c2420286389e7fed78ff361ec30e2f1d58c8560af8d64df5e7b61e0/sphinxcontrib_bibtex-2.7.0.tar.gz", hash = "sha256:fee700f7aae29bb8f654c62913f00d34ac44fc0b8ca0fa67ac922ff4453addee", size = 120669, upload-time = "2026-05-06T09:29:24.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c0/d28e62407f4733bbe0169287bc012f0ac3b4a2021066b285570654119c8b/sphinxcontrib_bibtex-2.7.0-py3-none-any.whl", hash = "sha256:28cf0ec7a957d1c7548d5749317ed472ce877e1b629f430f88e3789aa51f87b1", size = 40287, upload-time = "2026-05-06T09:29:23.253Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/75/3a1cc926da8c563c58ddc124a7b3fe5ccadcae96c96e3a6f8ac3653a210a/sphinxcontrib_mermaid-2.0.2.tar.gz", hash = "sha256:f09576c78ca93fa0e3034fd9c45aaffa7c44ab449de9c43b8b8d262afe52bc66", size = 19265, upload-time = "2026-05-05T13:59:02.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/93be7e0f7fa915a576859b3bfac7a7baa3303181c44d7db7eefbd3e8a69f/sphinxcontrib_mermaid-2.0.2-py3-none-any.whl", hash = "sha256:d862e514991279fb4816302c5cfe167d2557bf3ce7125ae0cb47dac80a0f46ce", size = 14094, upload-time = "2026-05-05T13:59:01.585Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, +] + +[[package]] +name = "superqt" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "qtpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/fe/919245507b15b4633cd40292d69c3b6afa8a650bf3ec33f3329d26314a3c/superqt-0.8.2.tar.gz", hash = "sha256:faa3bd00ef1c9b209b1f31b154dd41c596fec3824f61a70c251fb5569acd7ba6", size = 110190, upload-time = "2026-05-18T14:33:47.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/b9/748c3cbcdba20ef01c055a35905cad5c771cc1df29be11b9a82da6fd8e0d/superqt-0.8.2-py3-none-any.whl", hash = "sha256:ce8400ae7b577d090368aae5bdbabb06cd6bc893f6ca73485bb686835bf20943", size = 101595, upload-time = "2026-05-18T14:33:45.462Z" }, +] + +[package.optional-dependencies] +iconify = [ + { name = "pyconify" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tables" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "blosc2", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numexpr", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "py-cpuinfo", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/96708a84e9fcd29d1f684d56d4c38a23d29b1c934599a072a49f27ccfa71/tables-3.10.1.tar.gz", hash = "sha256:4aa07ac734b9c037baeaf44aec64ec902ad247f57811b59f30c4e31d31f126cf", size = 4762413, upload-time = "2024-08-17T09:57:47.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/69/a768ec8104ada032c9be09f521f548766ddd0351bc941c9d42fa5db001de/tables-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca9d11a570ca1bc57f0845e54e55c3093d5a1ace376faee639e09503a73745b", size = 6823691, upload-time = "2024-08-17T09:56:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2d/074bc14b39de9b552eec02ee583eff2997d903da1355f4450506335a6055/tables-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b62881cb682438d1e92b9178db42b160638aef3ca23341f7d98e9b27821b1eb4", size = 5471221, upload-time = "2024-08-17T09:56:54.84Z" }, + { url = "https://files.pythonhosted.org/packages/4a/30/29411ab804b5ac4bee25c82ba38f4e7a8c0b52c6a1cdbeea7d1db33a53fe/tables-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cf1bfd8b0e0195196205fc8a134628219cff85d20da537facd67a291e6b347", size = 7170201, upload-time = "2024-08-17T09:56:59.011Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7d/3165c7538b8e89b22fa17ad68e04106cca7023cf68e94011ae7b3b6d2a78/tables-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f0e6dd45b91d99bf3976c8655c48fe3816baf390b9098e4fb2f0fdf9da7078", size = 7571035, upload-time = "2024-08-17T09:57:03.115Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/985a23d2cf27aad383301a5e99e1851228a1941b868515612b5357bded5f/tables-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90542ec172d1d60df0b796c48ad446f2b69a5d5cd3077bd6450891b854d1ffb", size = 6311650, upload-time = "2024-08-17T09:57:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/dc/04/957264eb35e60251830a965e2d02332eb36ed14fbd8345df06981bbf3ece/tables-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8917262a2bb3cd79d37e108557e34ec4b365fdcc806e01dd10765a84c65dab6", size = 6790492, upload-time = "2024-08-17T09:57:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/b2/19/eb7af9d92aaf6766f5fedfce11a97ab03cf39856561c5f562dc0c769a682/tables-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f93f6db623b484bb6606537c2a71e95ee34fae19b0d891867642dd8c7be05af6", size = 5506835, upload-time = "2024-08-17T09:57:13.883Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/897324e1ad543ca439b2c91f04c406f3eeda6e7ff2f43b4cd939f05043e4/tables-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01ca51624bca1a87e703d6d6b796368bc3460ff007ea8b1341be03bedd863833", size = 7166960, upload-time = "2024-08-17T09:57:17.463Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5c/3f21d1135bf60af99ac79a17bbffd333d69763df2197ba04f47dd30bbd4e/tables-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9372516c76be3a05a573df63a69ce38315d03b5816d2a1e89c48129ec8b161b0", size = 7568724, upload-time = "2024-08-17T09:57:23.02Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/3ee6b66263902eccadc4e0e23bca7fb480fd190904b7ce0bea4777b5b799/tables-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:09190fb504888aeacafb7739c13d5c5a3e87af3d261f4d2f832b1f8407be133a", size = 6312200, upload-time = "2024-08-17T09:57:26.322Z" }, + { url = "https://files.pythonhosted.org/packages/95/ec/ea6c476e33602c172c797fe8f8ab96d007d964137068276d142b142a28e5/tables-3.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7090af37909e3bf229d5599fa442633e5a93b6082960b01038dc0106e07a8da", size = 6791597, upload-time = "2024-08-17T09:57:29.598Z" }, + { url = "https://files.pythonhosted.org/packages/74/02/a967a506e9204e3328a8c03f67e6f3c919defc8df11aba83ae5b2abf7b0f/tables-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:203ed50c0c5f30f007df7633089b2a567b99856cd25d68f19d91624a8db2e7ad", size = 5474779, upload-time = "2024-08-17T09:57:32.43Z" }, + { url = "https://files.pythonhosted.org/packages/c3/26/925793f753664ec698b2c6315c818269313db143da38150897cf260405c2/tables-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e36ce9f10471c69c1f0b06c6966de762558a35d62592c55df7994a8019adaf0c", size = 7130683, upload-time = "2024-08-17T09:57:36.181Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/2b34f22284459e940a84e71dba19b2a34c7cc0ce3cdf685923c50d5b9611/tables-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f233e78cc9fa4157ec4c3ef2abf01a731fe7969bc6ed73539e5f4cd3b94c98b2", size = 7531367, upload-time = "2024-08-17T09:57:39.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/27/5a23830f611e26dd7ee104096c6bb82e481b16f3f17ccaed3075f8d48312/tables-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:34357d2f2f75843a44e6fe54d1f11fc2e35a8fd3cb134df3d3362cff78010adb", size = 6295046, upload-time = "2024-08-17T09:57:43.561Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/e7c25df877e054b05f146d6ccb920bcdbe8d39b35a0962868b80547532c7/tables-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6fc5b46a4f359249c3ab9a0a0a2448d7e680e68cffd63fdf3fb7171781edd46e", size = 6824253, upload-time = "2024-11-09T19:26:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/091865d75090a24493bd1b66e52d72f4d9627ff42983a13d4dcd89455d02/tables-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ecabd7f459d40b7f9f5256850dd5f43773fda7b789f827de92c3d26df1e320f", size = 5499587, upload-time = "2024-11-09T19:26:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/9dac8af333149fa01add439f710d4a312b70faf81c2f59a16b8bfaebb75e/tables-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40a4ee18f3c9339d9dd8fd3777c75cda5768f2ff347064a2796f59161a190af8", size = 7128236, upload-time = "2024-11-09T19:26:15.716Z" }, + { url = "https://files.pythonhosted.org/packages/89/fd/62f31643596f6ab71fc6d2a87acdee0bc01a03fbe1a7f3f6dc0c91e2546d/tables-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757c6ea257c174af8036cf8f273ede756bbcd6db5ac7e2a4d64e788b0f371152", size = 7527953, upload-time = "2024-11-09T19:26:20.229Z" }, +] + +[[package]] +name = "tables" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "blosc2", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numexpr", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "py-cpuinfo", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/a3/d213ebe7376d48055bd55a29cd9f99061afa0dcece608f94a5025d797b0a/tables-3.11.1.tar.gz", hash = "sha256:78abcf413091bc7c1e4e8c10fbbb438d1ac0b5a87436c5b972c3e8253871b6fb", size = 4790533, upload-time = "2026-03-01T11:43:36.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/bb/4a9cde6628563388db26fa86c64adb0f2475a757e72af0ec185fd520b72f/tables-3.11.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:eb30684c42a77bbecdef2b9c763c4372b0ddc9cc5bd8b2a2055f2042eee67217", size = 7045977, upload-time = "2026-03-01T11:42:48.605Z" }, + { url = "https://files.pythonhosted.org/packages/78/74/6568c8d3aabf9982ab89fe3e378afbd7aad4894bde4570991a3246169ef4/tables-3.11.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0367d2e3df0f10ea63ccf4279f3fe58e32ec481767320301a483e2b3cd83efc", size = 6264947, upload-time = "2026-03-01T11:42:53.192Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/ec228901fca4c996306b17f5c60a4105144df0bbd07b3a4a816f91f37b4a/tables-3.11.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56bf6fb9132ead989b7e76695d7613d6d08f071a8019038d6565ba90c66b9f3e", size = 6903733, upload-time = "2026-03-01T11:42:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/c2dc674ea70fa9a4819417289a9c0d3e4780835beeed573eb66964cfb763/tables-3.11.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e78fe190fdeb4afe430b79651bae2a4f341904eb85aa8dbafe5f1caee1c7f67", size = 7241357, upload-time = "2026-03-01T11:43:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/a59b62af4127790c618eb11c06c106706e07509a3fb9e346b2a3ffa74419/tables-3.11.1-cp311-abi3-win_amd64.whl", hash = "sha256:7fa6cb03f6fe55ae4f85e89ec5450e5c40cc4c52d8c3b60eb157a445c2219e89", size = 6526565, upload-time = "2026-03-01T11:43:08.58Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/561c82496e7c8c15ebf19b53b12c0ef91b322a66869db762db9711102764/tables-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a4bbd95036a4d0cc5c86c1f87fbb490b4c53cd70982f1c01b3ed6dcb3085cbb9", size = 7111409, upload-time = "2026-03-01T11:43:13.424Z" }, + { url = "https://files.pythonhosted.org/packages/84/18/bac920aee8239b572c506459607c6dd8742bc6275a43d51d2dd6ae1a1541/tables-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e3cfe79484351f7216eb8f3767bfa1217bfd271b04428f79cfa7ef6d7491919d", size = 6380142, upload-time = "2026-03-01T11:43:17.213Z" }, + { url = "https://files.pythonhosted.org/packages/59/3c/f4a694aa744d2b14d536e172c28dd70c84445f4787083a82d6d44a39e39f/tables-3.11.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a9c35f87fcb6a48c79fbc4e3ab15ca8f6053c4ce13063d6ca2ec36cbb58f40f", size = 7014135, upload-time = "2026-03-01T11:43:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/94d4320d6c0fe5bd55230eec90cd142d58cda37b7cce00a318ac2a6abd93/tables-3.11.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cf3218b76ba78d156d6ee75c19fb757d50682f6c7b4905370441afbfc9d77f3", size = 7349293, upload-time = "2026-03-01T11:43:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/f7/02/a0f61a602ce2f2be8cc2e6146cc51acdaa8a1bb9b823b3863e70d3e0505d/tables-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a6f7a3b82dbf0ae0f30de635ca88bb42dd87938b0950369d0ee4289c52ae6de2", size = 6854713, upload-time = "2026-03-01T11:43:31.934Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-auth", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-auth-oauthlib", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/a2/66ed644f6ed1562e0285fcd959af17670ea313c8f331c46f79ee77187eb9/tensorboard-2.14.1-py3-none-any.whl", hash = "sha256:3db108fb58f023b6439880e177743c5f1e703e9eeb5fb7d597871f949f85fd58", size = 5508920, upload-time = "2023-09-27T23:37:16.71Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/41/dccba8c5f955bc35b6110ff78574e4e5c8226ad62f08e732096c3861309b/tensorboard-2.17.1-py3-none-any.whl", hash = "sha256:253701a224000eeca01eee6f7e978aea7b408f60b91eb0babdb04e78947b773e", size = 5502989, upload-time = "2024-08-14T18:10:47.657Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "markdown", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard-data-server", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "werkzeug", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/de/021c1d407befb505791764ad2cbd56ceaaa53a746baed01d2e2143f05f18/tensorboard-2.18.0-py3-none-any.whl", hash = "sha256:107ca4821745f73e2aefa02c50ff70a9b694f39f790b11e6f682f7d326745eab", size = 5503036, upload-time = "2024-09-25T21:21:50.169Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "markdown", marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pillow", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "tensorboard-data-server", marker = "python_full_version < '3.13'" }, + { name = "werkzeug", marker = "python_full_version < '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-estimator", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.12' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'x86_64' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.37.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.12' and sys_platform == 'darwin' and extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf') or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/51/ad9ebf4ef29754b813a057d64a0634feb12aef27cabcbdb7433dc5cd4cb4/tensorflow-2.14.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:318b21b18312df6d11f511d0f205d55809d9ad0f46d5f9c13d8325ce4fe3b159", size = 229634719, upload-time = "2023-09-26T22:51:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e0/1db7b4b382e7d654dd176ee3e09af201f0735ea1a3233c087c3e63f054e9/tensorflow-2.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:927868c9bd4b3d2026ac77ec65352226a9f25e2d24ec3c7d088c68cff7583c9b", size = 2108, upload-time = "2023-09-26T22:53:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4a/40/da089d1cabd9141543dfeb462e16f6c6741a76ac326174f168b7ce53d54f/tensorflow-2.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3870063433aebbd1b8da65ed4dcb09495f9239397f8cb5a8822025b6bb65e04", size = 2122, upload-time = "2023-09-26T22:56:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7a/c7762c698fb1ac41a7e3afee51dc72aa3ec74ae8d2f57ce19a9cded3a4af/tensorflow-2.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c9c1101269efcdb63492b45c8e83df0fc30c4454260a252d507dfeaebdf77ff", size = 489833115, upload-time = "2023-09-26T22:53:51.969Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c3/17c6aa1dd5bc8cea5bf00d0c3a021a5dd1680c250861cc877a7e556e4b9b/tensorflow-2.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:0b7eaab5e034f1695dc968f7be52ce7ccae4621182d1e2bf6d5b3fab583be98c", size = 2099, upload-time = "2023-09-26T22:55:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/22/50/1e211cbb5e1f52e55eeae1605789c9d24403962d37581cf0deb3e6b33377/tensorflow-2.14.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:00c42e7d8280c660b10cf5d0b3164fdc5e38fd0bf16b3f9963b7cd0e546346d8", size = 229677851, upload-time = "2023-09-26T22:51:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/de/ea/90267db2c02fb61f4d03b9645c7446d3cbca6d5c08522e889535c88edfcd/tensorflow-2.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c92f5526c2029d31a036be06eb229c71f1c1821472876d34d0184d19908e318c", size = 2106, upload-time = "2023-09-26T22:53:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/92/ba/0b9dc0a69e518cca919587fd32ec22a81c99bcdf94c8482f00440fff72d0/tensorflow-2.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c224c076160ef9f60284e88f59df2bed347d55e64a0ca157f30f9ca57e8495b0", size = 2122, upload-time = "2023-09-26T22:56:28.206Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/25e76075081ea98ec48f23929cefee58be0b42212e38074a9ec5c19e838c/tensorflow-2.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80cabe6ab5f44280c05533e5b4a08e5b128f0d68d112564cffa3b96638e28aa", size = 489875759, upload-time = "2023-09-26T22:54:19.219Z" }, + { url = "https://files.pythonhosted.org/packages/80/6f/57d36f6507e432d7fc1956b2e9e8530c5c2d2bfcd8821bcbfae271cd6688/tensorflow-2.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:0587ece626c4f7c4fcb2132525ea6c77ad2f2f5659a9b0f4451b1000be1b5e16", size = 2099, upload-time = "2023-09-26T22:55:30.95Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/81/d658100476affe0c68ccfb0ad72813ef487578187cadba1c896a26cac8e0/tensorflow-2.17.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:61f45ca991cf3dddf0b1069674c455fdbf38edf749dab962bb4bb8a3f99fb25f", size = 236135046, upload-time = "2024-10-24T22:57:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/dc5fa720f2381f6dfeb955dfc330261e183ef91e57128cb39aecb8440d1c/tensorflow-2.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aa202e17894dcb0582283e5a5c703391d793ccce11c5c02b1fe8f839ae09f3c", size = 223903722, upload-time = "2024-10-24T22:57:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/ca18413eb576ee0231a1fe6e9d9499afcef614fd94154e0aaf14f32ba3eb/tensorflow-2.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618cc21b0adf695fc8b4323f56ccd17c1408379422e1a177481d4fd8523fa8e", size = 601258535, upload-time = "2024-10-24T22:57:50.405Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/28abaa2e4e6df19d77a2d41cc2b62589250c403dcd505c45433919526fd7/tensorflow-2.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c23498b370c9d6b2521722b6acc89fab61e6a593e886df16bed3075584bf1c7", size = 7521, upload-time = "2024-10-24T22:58:03.64Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/92db1ecf5550f8937c21c28f84228959adb9ccba5f19e283ec7f22825b11/tensorflow-2.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:595c220b0febe2295a150e0d870c742a75c56b145a63ed878f78d64aa43c6ca6", size = 236176925, upload-time = "2024-10-24T22:58:08.728Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/d1f227b56a82cf168d4d5b3fa6094857079f36891af4bc851281afa5cc23/tensorflow-2.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc41bc3e31d205dcf6e4b8afc0514de05445303d93fade03549085ef8c45a2b2", size = 223945776, upload-time = "2024-10-24T22:58:18.161Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/f5b1f4b2c08295ae1cb8760d1fb6043485459f0d8c107dd900e76a6ba25d/tensorflow-2.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68841e17e573301d8ba9192f929b8096b0b341567cb81414096305c723de68d0", size = 601300475, upload-time = "2024-10-24T22:58:31.181Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e6/8dbd20c4942b578aa18ef61e8d7858ddbd3650bbea731539c1fdadbaa466/tensorflow-2.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:a6bd9474f1e0dedb7deb331c8e93cf2d5997da8781a1949f75c4a7cf8923d2e3", size = 7518, upload-time = "2024-10-24T22:58:42.649Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/29bfb68a1cfd61649cd51f5f08474a9eab343c2402f8998af6f1f6120aa5/tensorflow-2.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d43698039fd057ee6d7c3f908c4e9a6e310be6e77adda419bae3fee5ca7192fe", size = 236283570, upload-time = "2024-10-24T22:58:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/3a/86/3af00483ff5215e52ebd9d5be2987798a6985f60db413091522a97459c2c/tensorflow-2.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:695741b80ea9b2603a8d3f423051476c02a9cf48f4721d18c4dafbe0a8a5ca91", size = 224039295, upload-time = "2024-10-24T22:58:55.317Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/c81ec3b1ca9980cba8a8123a4df0a3f3b96ae4f49d024cd0c4175aa443c0/tensorflow-2.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3452ab09940cbc3be896641b256855ba92154b79a90c864c5b2be79db78a64", size = 601418583, upload-time = "2024-10-24T22:59:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/5d/53/0eafd55dd5802c60ac69f6ad01dec61cdb894b591a1f3e471e2a01edbe99/tensorflow-2.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:faf032fae35de0071f20850abd23d78e0e3ae116ce7fbfb9d034da2acc900543", size = 7521, upload-time = "2024-10-24T22:59:21.763Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow-io-gcs-filesystem", version = "0.37.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/e8/d5d54e18ff6fe67c75c3c65415c98ecd31bd0ff7613d47a1390f062993b5/tensorflow-2.18.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8da90a9388a1f6dd00d626590d2b5810faffbb3e7367f9783d80efff882340ee", size = 239373575, upload-time = "2024-10-25T00:14:11.549Z" }, + { url = "https://files.pythonhosted.org/packages/5a/58/99ba9d580c218fd866e6044b10915eb415f60af38c03dca6ff2df7f83337/tensorflow-2.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:589342fb9bdcab2e9af0f946da4ca97757677e297d934fcdc087e87db99d6353", size = 231677108, upload-time = "2024-10-25T00:14:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/d4/80/1567ccc375ccda4d28af28c960cca7f709f7c259463ac1436554697e8868/tensorflow-2.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb77fae50d699442726d1b23c7512c97cd688cc7d857b028683d4535bbf3709", size = 615262200, upload-time = "2024-10-25T00:14:39.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/63/5ca1b06cf17dda9c52927917a7911612953a7d91865b1288c7f9eac2b53b/tensorflow-2.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f5a8b4e6273f488dc069fc3ac2211b23acd3d0437d919349c787fa341baa8a", size = 7519, upload-time = "2024-10-25T00:14:52.683Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/556c4159675c1a30e077ec2a942eeeb81b457cc35c247a5b4a59a1274f05/tensorflow-2.18.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:453cb60638a02fd26316fb36c8cbcf1569d33671f17c658ca0cf2b4626f851e7", size = 239492146, upload-time = "2024-10-25T00:14:56.741Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3d/45956345442e3a7b335df6f13d068121d8454c243f31b1f44244705ac584/tensorflow-2.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85f1e7369af6d329b117b52e86093cd1e0458dd5404bf5b665853f873dd00b48", size = 231839918, upload-time = "2024-10-25T00:15:05.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/76/c55967ac9968ddaede25a4dce37aba37e9030656f02c12676151ce1b6f22/tensorflow-2.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b8dd70fa3600bfce66ab529eebb804e1f9d7c863d2f71bc8fe9fc7a1ec3976", size = 615407268, upload-time = "2024-10-25T00:15:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/271e77c22724f370c24c705f394b8035b4d27e4c2c6339f3f45ab9b8258e/tensorflow-2.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e8b0f499ef0b7652480a58e358a73844932047f21c42c56f7f3bdcaf0803edc", size = 7516, upload-time = "2024-10-25T00:15:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/4cc283db323fd723f630e2454b2857054d2c81ff5012c1857659e72470f1/tensorflow-2.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ec4133a215c59314e929e7cbe914579d3afbc7874d9fa924873ee633fe4f71d0", size = 239565465, upload-time = "2024-10-25T00:15:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/56/e4/55aaac2b15af4dad079e5af329a79d961e5206589d0e02b1e8da221472ed/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4822904b3559d8a9c25f0fe5fef191cfc1352ceca42ca64f2a7bc7ae0ff4a1f5", size = 231898760, upload-time = "2024-10-25T00:15:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/50/29/61ce80da0bfea3948326697dd1d832d28c863c9dacf90a27ee80fd4c1d31/tensorflow-2.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfdd65ea7e064064283dd78d529dd621257ee617218f63681935fd15817c6286", size = 615520727, upload-time = "2024-10-25T00:16:01.386Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f1/828bbccc84a72db960a7d116f55f3f6aec9f5658f5d32ce9db20142d5742/tensorflow-2.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:a701c2d3dca5f2efcab315b2c217f140ebd3da80410744e87d77016b3aaf53cb", size = 7520, upload-time = "2024-10-25T00:16:12.418Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "astunparse", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "flatbuffers", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "gast", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "google-pasta", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "grpcio", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "h5py", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "opt-einsum", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "packaging", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorboard", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/5e/955a719c2359430a6fa6ec596bafc903b31285844ef44ae53e83bb91ac62/tensorflow-2.18.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8baba2b0f9f286f8115a0005d17c020d2febf95e434302eaf758f2020c1c4de5", size = 239430540, upload-time = "2025-03-12T00:11:40.574Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/4631df86b7880c18ce221b16e9f6f08e8100143d99d68bd6612d8ec534f8/tensorflow-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd7284768f5a6b10e41a700e8141de70756dc62ed5d0b93360d131ccc0a6ba8", size = 231774989, upload-time = "2025-03-12T00:11:50.957Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/43ed0e0accc63747cb9b6250cbef6515a449f848d4eda0af9d591ac1cea9/tensorflow-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f929842999d60e7da67743ae5204b477259f3b771c02e5e437d232267e49f18", size = 615365234, upload-time = "2025-03-12T00:12:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e8/27/75d313117d8a9f8aadb8b9121cc33d44793a2d704c3b3f5866e632821b82/tensorflow-2.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:db1d186c17b6a7c51813e275d0a83e964669822372aa01d074cf64b853ee76ac", size = 368995257, upload-time = "2025-03-12T00:12:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/57e2acd11a2587cc5c0a6612a389a57f3bda3cd60d132934cb7a9bb00a81/tensorflow-2.18.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:661029cd769b311db910b79a3a6ef50a5a61ecc947172228c777a49989722508", size = 239549037, upload-time = "2025-03-12T00:12:38.202Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b3/902588dcffbc0603893f1df482840ff9c596430155d62e159bc8fc155230/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a6485edd2148f70d011dbd1d8dc2c775e91774a5a159466e83d0d1f21580944", size = 231937898, upload-time = "2025-03-12T00:12:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/05d862ebeaaf63343dffc4f97dab62ac493e8c2bbc6b1a256199bcc78ed4/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9f87e5d2a680a4595f5dc30daf6bbaec9d4129b46d7ef1b2af63c46ac7d2828", size = 615510377, upload-time = "2025-03-12T00:13:03.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/5f5ade4be81e521a16e143234747570ffd0d1a90e001ecc2688aa54bb419/tensorflow-2.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:99223d0dde08aec4ceebb3bf0f80da7802e18462dab0d5048225925c064d2af7", size = 369183850, upload-time = "2025-03-12T00:13:24.786Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/1cad54f8634897ad9421de8f558df9aa63d3f2747eb803ce5dbb2db1ef5b/tensorflow-2.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:98afa9c7f21481cdc6ccd09507a7878d533150fbb001840cc145e2132eb40942", size = 239622377, upload-time = "2025-03-12T00:13:36.89Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/35a3542a91f4ffd4cf01e72d7f0fb59596cd5f467ff64878b0caef8e0f31/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ba52b9c06ab8102b31e50acfaf56899b923171e603c8942f2bfeb181d6bb59e", size = 231996787, upload-time = "2025-03-12T00:13:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/64/42/812539a8878c242eb0bacf106f5ea8936c2cc4d7f663868bd872a79772ac/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442d2a774811789a8ad948e7286cb950fe3d87d3754e8cc6449d53b03dbfdaa6", size = 615623178, upload-time = "2025-03-12T00:14:03.541Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/9c5e935b76eebdf46df524980d49700a9c9af56abc8c62bfd93f57709563/tensorflow-2.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:210baf6d421f3e044b6e09efd04494a33b75334922fe6cf11970e2885172620a", size = 369234070, upload-time = "2025-03-12T00:14:23.423Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "astunparse", marker = "python_full_version < '3.13'" }, + { name = "flatbuffers", marker = "python_full_version < '3.13'" }, + { name = "gast", marker = "python_full_version < '3.13'" }, + { name = "google-pasta", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "h5py", marker = "python_full_version < '3.13'" }, + { name = "keras", version = "3.12.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "keras", version = "3.14.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.11' and python_full_version < '3.13' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "libclang", marker = "python_full_version < '3.13'" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "opt-einsum", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "requests", marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "six", marker = "python_full_version < '3.13'" }, + { name = "tensorboard", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "termcolor", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", version = "2.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/0e/9408083cb80d85024829eb78aa0aa799ca9f030a348acac35631b5191d4b/tensorflow-2.20.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e5f169f8f5130ab255bbe854c5f0ae152e93d3d1ac44f42cb1866003b81a5357", size = 200387116, upload-time = "2025-08-13T16:50:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ea91ac67a9fd36d3372099f5a3e69860ded544f877f5f2117802388f4212/tensorflow-2.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02a0293d94f5c8b7125b66abf622cc4854a33ae9d618a0d41309f95e091bbaea", size = 259307122, upload-time = "2025-08-13T16:50:47.909Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9e/0d57922cf46b9e91de636cd5b5e0d7a424ebe98f3245380a713f1f6c2a0b/tensorflow-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7abd7f3a010e0d354dc804182372779a722d474c4d8a3db8f4a3f5baef2a591e", size = 620425510, upload-time = "2025-08-13T16:51:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/74/b5/d40e1e389e07de9d113cf8e5d294c04d06124441d57606febfd0fb2cf5a6/tensorflow-2.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a69ac2c2ce20720abf3abf917b4e86376326c0976fcec3df330e184b81e4088", size = 331664937, upload-time = "2025-08-13T16:51:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/de33bd90dbddc8eede8f99ddeccfb374f7e18f84beb404bfe2cbbdf8df90/tensorflow-2.20.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5f964016c5035d09b85a246a6b739be89282a7839743f3ea63640224f0c63aee", size = 200507363, upload-time = "2025-08-13T16:51:28.27Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a3d455db88ab5b35ce53ab885ec0dd9f28d905a86a2250423048bc8cafa0/tensorflow-2.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e9568c8efcb05c0266be223e3269c62ebf7ad3498f156438311735f6fa5ced5", size = 259465882, upload-time = "2025-08-13T16:51:39.546Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/7df285ee8a88139fab0b237003634d90690759fae9c18f55ddb7c04656ec/tensorflow-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:481499fd0f824583de8945be61d5e827898cdaa4f5ea1bc2cc28ca2ccff8229e", size = 620570129, upload-time = "2025-08-13T16:51:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/9246d3c7e185a29d7359d8b12b3d70bf2c3150ecf1427ec1382290e71a56/tensorflow-2.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:7551558a48c2e2f6c32a1537f06c654a9df1408a1c18e7b99c3caafbd03edfe3", size = 331845735, upload-time = "2025-08-13T16:52:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/47712f425c09cc8b8dba39c6c45aee939c4636a6feb8c81376a4eae653e0/tensorflow-2.20.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:52b122f0232fd7ab10f28d537ce08470d0b6dcac7fff9685432daac7f8a06c8f", size = 200540302, upload-time = "2025-08-13T16:52:22.146Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109, upload-time = "2025-08-13T16:52:31.513Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547, upload-time = "2025-08-13T16:52:46.396Z" }, + { url = "https://files.pythonhosted.org/packages/f9/37/b97abb360b551fbf5870a0ee07e39ff9c655e6e3e2f839bc88be81361842/tensorflow-2.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:1590cbf87b6bcbd34d8e9ad70d0c696135e0aa71be31803b27358cf7ed63f8fc", size = 331887041, upload-time = "2025-08-13T16:53:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/af283f402f8d1e9315644a331a5f0f326264c5d1de08262f3de5a5ade422/tensorflow-2.20.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:197f0b613b38c0da5c6a12a8295ad4a05c78b853835dae8e0f9dfae3ce9ce8a5", size = 200671458, upload-time = "2025-08-13T16:53:16.568Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4c/c1aa90c5cc92e9f7f9c78421e121ef25bae7d378f8d1d4cbad46c6308836/tensorflow-2.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47c88e05a07f1ead4977b4894b3ecd4d8075c40191065afc4fd9355c9db3d926", size = 259663776, upload-time = "2025-08-13T16:53:24.507Z" }, + { url = "https://files.pythonhosted.org/packages/43/fb/8be8547c128613d82a2b006004026d86ed0bd672e913029a98153af4ffab/tensorflow-2.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa3729b0126f75a99882b89fb7d536515721eda8014a63e259e780ba0a37372", size = 620815537, upload-time = "2025-08-13T16:53:42.577Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9e/02e201033f8d6bd5f79240b7262337de44c51a6cfd85c23a86c103c7684d/tensorflow-2.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:c25edad45e8cb9e76366f7a8c835279f9169028d610f3b52ce92d332a1b05438", size = 332012220, upload-time = "2025-08-13T16:53:57.303Z" }, +] + +[[package]] +name = "tensorflow-estimator" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/da/4f264c196325bb6e37a6285caec5b12a03def489b57cc1fdac02bb6272cd/tensorflow_estimator-2.14.0-py2.py3-none-any.whl", hash = "sha256:820bf57c24aa631abb1bbe4371739ed77edb11361d61381fd8e790115ac0fd57", size = 440664, upload-time = "2023-09-11T18:41:50.481Z" }, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/f1/30c558bf7e795d02415e6e836f6190367130e0dfb8de585b8b81ddde5c7f/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a71421f8d75a093b6aac65b4c8c8d2f768c3ca6215307cf8c16192e62d992bcf", size = 1642401, upload-time = "2023-02-25T19:31:32.234Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/802488d94fdd14832d9832bd1fbd89cebac2519270d99ce1fe1c75eeb4b2/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359134ecbd3bf938bb0cf65be4526106c30da461b2e2ce05446a229ed35f6832", size = 2381672, upload-time = "2023-02-25T19:31:34.644Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/b18c800a17a10abeae3e8aa420fb452646c8f501bac82d61626437c48b0e/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b658b33567552f155af2ed848130f787bfda29381fa78cd905d5ee8254364f3c", size = 2572150, upload-time = "2023-02-25T19:31:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/437068ed6b44162d54addb8ac0ddfe9e406d07ac6f9c8a6cf96869ec2262/tensorflow_io_gcs_filesystem-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:961353b38c76471fa296bb7d883322c66b91415e7d47087236a6706db3ab2758", size = 1486315, upload-time = "2023-02-25T19:31:38.297Z" }, + { url = "https://files.pythonhosted.org/packages/84/00/900ca310ff2e46eb3127f8f54af0b0000a6cc786be6a54dc2cfe841f4683/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8909c4344b0e96aa356230ab460ffafe5900c33c1aaced65fafae71d177a1966", size = 1642401, upload-time = "2023-02-25T19:31:40.204Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/0d44ef93add3432ce43f37fe0c205cc7b6fd685fca80054fb4a646a9dbe3/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e417faf8755aafe52d8f8c6b5ae5bae6e4fae8326ee3acd5e9181b83bbfbae87", size = 2381673, upload-time = "2023-02-25T19:31:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/3064195efa016fff942009fe965ecbbbbd7d70bf34ee22d4ff31a0f3443a/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c40e3c4ee1f8dda3b545deea6b8839192c82037d8021db9f589908034ad975", size = 2572150, upload-time = "2023-02-25T19:31:43.874Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/9566a313927be582ca99455a9523a097c7888fc819695bdc08415432b202/tensorflow_io_gcs_filesystem-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:4bb37d23f21c434687b11059cb7ffd094d52a7813368915ba1b7057e3c16e414", size = 1486315, upload-time = "2023-02-25T19:31:45.641Z" }, +] + +[[package]] +name = "tensorflow-io-gcs-filesystem" +version = "0.37.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/a3/12d7e7326a707919b321e2d6e4c88eb61596457940fd2b8ff3e9b7fac8a7/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:249c12b830165841411ba71e08215d0e94277a49c551e6dd5d72aab54fe5491b", size = 2470224, upload-time = "2024-07-01T23:44:15.341Z" }, + { url = "https://files.pythonhosted.org/packages/1c/55/3849a188cc15e58fefde20e9524d124a629a67a06b4dc0f6c881cb3c6e39/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:257aab23470a0796978efc9c2bcf8b0bc80f22e6298612a4c0a50d3f4e88060c", size = 3479613, upload-time = "2024-07-01T23:44:17.445Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/9095c69e22c879cb3896321e676c69273a549a3148c4f62aa4bc5ebdb20f/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8febbfcc67c61e542a5ac1a98c7c20a91a5e1afc2e14b1ef0cb7c28bc3b6aa70", size = 4842078, upload-time = "2024-07-01T23:44:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/48/47b7d25572961a48b1de3729b7a11e835b888e41e0203cca82df95d23b91/tensorflow_io_gcs_filesystem-0.37.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9679b36e3a80921876f31685ab6f7270f3411a4cc51bc2847e80d0e4b5291e27", size = 5085736, upload-time = "2024-07-01T23:44:21.034Z" }, + { url = "https://files.pythonhosted.org/packages/40/9b/b2fb82d0da673b17a334f785fc19c23483165019ddc33b275ef25ca31173/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:32c50ab4e29a23c1f91cd0f9ab8c381a0ab10f45ef5c5252e94965916041737c", size = 2470224, upload-time = "2024-07-01T23:44:23.039Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cc/16634e76f3647fbec18187258da3ba11184a6232dcf9073dc44579076d36/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b02f9c5f94fd62773954a04f69b68c4d576d076fd0db4ca25d5479f0fbfcdbad", size = 3479613, upload-time = "2024-07-01T23:44:24.399Z" }, + { url = "https://files.pythonhosted.org/packages/de/bf/ba597d3884c77d05a78050f3c178933d69e3f80200a261df6eaa920656cd/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e1f2796b57e799a8ca1b75bf47c2aaa437c968408cc1a402a9862929e104cda", size = 4842079, upload-time = "2024-07-01T23:44:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7c8ee5fe2fd8cb6392669ef16e71841133041fee8a330eff519ad9b36e4556", size = 5085736, upload-time = "2024-07-01T23:44:28.618Z" }, + { url = "https://files.pythonhosted.org/packages/70/83/4422804257fe2942ae0af4ea5bcc9df59cb6cb1bd092202ef240751d16aa/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ffebb6666a7bfc28005f4fbbb111a455b5e7d6cd3b12752b7050863ecb27d5cc", size = 2470224, upload-time = "2024-07-01T23:44:30.232Z" }, + { url = "https://files.pythonhosted.org/packages/43/9b/be27588352d7bd971696874db92d370f578715c17c0ccb27e4b13e16751e/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fe8dcc6d222258a080ac3dfcaaaa347325ce36a7a046277f6b3e19abc1efb3c5", size = 3479614, upload-time = "2024-07-01T23:44:32.316Z" }, + { url = "https://files.pythonhosted.org/packages/d3/46/962f47af08bd39fc9feb280d3192825431a91a078c856d17a78ae4884eb1/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbb33f1745f218464a59cecd9a18e32ca927b0f4d77abd8f8671b645cc1a182f", size = 4842077, upload-time = "2024-07-01T23:44:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9b/790d290c232bce9b691391cf16e95a96e469669c56abfb1d9d0f35fa437c/tensorflow_io_gcs_filesystem-0.37.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:286389a203a5aee1a4fa2e53718c661091aa5fea797ff4fa6715ab8436b02e6c", size = 5085733, upload-time = "2024-07-01T23:44:36.663Z" }, +] + +[[package]] +name = "tensorflow-metal" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin'" }, + { name = "wheel", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/da/463240cc8ff13a57119db62a676e2edca86fb93905a13527872dadf1926e/tensorflow_metal-1.2.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:bc735e36c97874e41f77ec2e7421ff745d2ec36ee641141c8091e4cc2dbcc819", size = 1357400, upload-time = "2025-01-31T00:52:54.577Z" }, + { url = "https://files.pythonhosted.org/packages/e0/09/91b253511cd59b9964672567f36b412daf3c70f75fcb5e84468fafa939ac/tensorflow_metal-1.2.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5fa7cee627031c14f45bd97ff0ef422cd6c3866199ff99cf29b94db6674ceb42", size = 1357400, upload-time = "2025-01-31T00:52:56.634Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/988b619322d5617a928e7f31cbb1ed8dd7f375f69dfa73dab26409a00382/tensorflow_metal-1.2.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4bece0ecb154b713b9f5ad4aec676a366d312822161e3cf0e1dea737c64cec04", size = 1357400, upload-time = "2025-01-31T00:52:57.924Z" }, +] + +[[package]] +name = "tensorpack" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "msgpack-numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "numpy", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "psutil", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pyzmq", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "six", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tabulate", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "termcolor", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tqdm", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/f0/edfda47ca6cc9ece30a893362c336b9121b691529e4cdf3b8732565be790/tensorpack-0.11.tar.gz", hash = "sha256:022b610e416e62e3575424cd08e60af27808a5fb6914294615391caf582cbd4f", size = 223526, upload-time = "2021-01-22T08:44:04.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/8c/63e5f5a4a04dea36b75850f9daa885ccbfad64bec1fae0ee4ca9f31b3eaa/tensorpack-0.11-py2.py3-none-any.whl", hash = "sha256:afcdaccf6e8e7d61c98970646d57b1c22372ddd712c462477a90f53e3994c4a1", size = 296324, upload-time = "2021-01-22T08:44:03.089Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/e7/883de3afc2032c89d6351fca7eb119ee197de21a2189e3889e9171d64862/tf_keras-2.14.1.tar.gz", hash = "sha256:4ae6871c4989720ea335d771b75b2e520d789ec1600302800bb5ed1a855af2fe", size = 1250245, upload-time = "2023-09-29T01:57:43.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/c7a98446afd921b7c4a0688e6eb30bf8f48040d069c349c772e7763636e6/tf_keras-2.14.1-py3-none-any.whl", hash = "sha256:903cf3f1739d3f92d2de80e07d2b36b32cc79f60b7affcee09a7f4721c02fca6", size = 1714936, upload-time = "2023-09-29T01:57:41.02Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/2b/d647100a2e80d159b020f1dbc2ef2c6787ed33c914951a63b3c88cd805d0/tf_keras-2.17.0.tar.gz", hash = "sha256:fda97c18da30da0f72a5a7e80f3eee343b09f4c206dad6c57c944fb2cd18560e", size = 1260098, upload-time = "2024-07-15T21:31:01.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/8b/75f7572ec0273ed8da50bc19defe08aaaafcc15fda3407db53f49acec814/tf_keras-2.17.0-py3-none-any.whl", hash = "sha256:cc97717e4dc08487f327b0740a984043a9e0123c7a4e21206711669d3ec41c88", size = 1724905, upload-time = "2024-07-15T21:30:58.941Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/a4/7d0acc28cde2b29b8114552ce3258dafdc6b2186d24bf8e912de713dd74f/tf_keras-2.18.0.tar.gz", hash = "sha256:ebf744519b322afead33086a2aba872245473294affd40973694f3eb7c7ad77d", size = 1260765, upload-time = "2024-10-24T22:58:06.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/ed/e08afca471299b04a34cd548e64e89d0153eda0e6cf9b715356777e24774/tf_keras-2.18.0-py3-none-any.whl", hash = "sha256:c431d04027eef790fcd3261cf7fdf93eb74f3cb32e05078b57b7f5a54bd53262", size = 1725427, upload-time = "2024-10-24T22:58:03.918Z" }, +] + +[[package]] +name = "tf-keras" +version = "2.20.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/38/6060f6c7472439bb3890b9094d69d31d9f8d5da123b16c738773e70fff91/tf_keras-2.20.1.tar.gz", hash = "sha256:884be5938fb0b2b53b1583c1ae2b660ef87215377c29b5b6a77fd221b472aeaf", size = 1254487, upload-time = "2025-09-04T21:23:41.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/6b/d9a8202bfe5c9e3b078cf550bafab962aa9d6b1a1f1180f0065399d4c9b2/tf_keras-2.20.1-py3-none-any.whl", hash = "sha256:3f0e0a34d9a4c8758f24fdc1053e6e335f16ab5534c7d34f1899b8924779760c", size = 1694335, upload-time = "2025-09-04T21:23:40.153Z" }, +] + +[[package]] +name = "tf-slim" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/97/b0f4a64df018ca018cc035d44f2ef08f91e2e8aa67271f6f19633a015ff7/tf_slim-1.1.0-py2.py3-none-any.whl", hash = "sha256:fa2bab63b3925bd42601102e7f178dce997f525742596bf404fa8a6918e146ff", size = 352133, upload-time = "2020-05-07T22:18:55.976Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, +] + +[[package]] +name = "timm" +version = "1.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torchvision", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torchvision", version = "0.28.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/54/ece85b0eef3700c90db8271a43669b05a0ebbe2edb1962329c34374a297e/timm-1.0.27.tar.gz", hash = "sha256:315dfe63186ca9fb7ff941268941231fd5be259f2b4bb4afa28560ae1015cb9a", size = 2439861, upload-time = "2026-05-08T19:38:36.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/2e/26bab7686ff4aed48f8f5f6c23e2aa37b7a37ddd9effe3aa61e908fd518f/timm-1.0.27-py3-none-any.whl", hash = "sha256:5ff07c9ddf53cbada88eab1c93ff175c64cab683b5a2fddf863bcee985926f89", size = 2589280, upload-time = "2026-05-08T19:38:35.034Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "torch" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cublas-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-cupti-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-nvrtc-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-runtime-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufft-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-curand-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusolver-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvtx-cu11", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "triton", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4d/17e07377c9c3d1a0c4eb3fde1c7c16b5a0ce6133ddbabc08ceef6b7f2645/torch-2.0.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ced00b3ba471856b993822508f77c98f48a458623596a4c43136158781e306a", size = 619913492, upload-time = "2023-05-08T16:35:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/4925decd863ce88ed9190a4bd872b01c146243ee68db08c72923984fe335/torch-2.0.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:359bfaad94d1cda02ab775dc1cc386d585712329bb47b8741607ef6ef4950747", size = 74032191, upload-time = "2023-05-08T17:03:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/c216fe520b877cf4fe03858c825cd2031ca3e81e455b89639c9b5ec91981/torch-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7c84e44d9002182edd859f3400deaa7410f5ec948a519cc7ef512c2f9b34d2c4", size = 172339532, upload-time = "2023-05-08T16:38:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/2e/27/5c912ccc490ec78585cd463198e80be27b53db77f02e7398b41305606399/torch-2.0.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:567f84d657edc5582d716900543e6e62353dbe275e61cdc36eda4929e46df9e7", size = 143409489, upload-time = "2023-05-08T16:41:49.802Z" }, + { url = "https://files.pythonhosted.org/packages/5a/77/778954c0aad4f7901a1ba02a129bca7467c64a19079108e6b1d6ce8ae575/torch-2.0.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:787b5a78aa7917465e9b96399b883920c88a08f4eb63b5a5d2d1a16e27d2f89b", size = 55830861, upload-time = "2023-05-08T17:01:35.636Z" }, + { url = "https://files.pythonhosted.org/packages/c8/21/25020cfdd9f564a72f400ee491610e50cb212e8add8031abaa959af6451e/torch-2.0.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e617b1d0abaf6ced02dbb9486803abfef0d581609b09641b34fa315c9c40766d", size = 619895009, upload-time = "2023-05-08T17:28:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/7273dea60a17c63d9eaef04ae8fee02351e0cb477e76df4ea211896ae124/torch-2.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b6019b1de4978e96daa21d6a3ebb41e88a0b474898fe251fd96189587408873e", size = 74042514, upload-time = "2023-05-08T17:29:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c8/f0dc8642e3ce0a3ae5f05e5149ab9df5375d569294f7be9a1ab1d95a1d76/torch-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbd68cbd1cd9da32fe5d294dd3411509b3d841baecb780b38b3b7b06c7754434", size = 172343262, upload-time = "2023-05-08T17:29:02.117Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/f20686a5ebd09c6feacced771cf4041a521c411c5bb10359580e9e491797/torch-2.0.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:ef654427d91600129864644e35deea761fb1fe131710180b952a6f2e2207075e", size = 143124494, upload-time = "2023-05-08T16:28:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/f901437d3e3ef6fe97adb1f372479626d994185b8fa06803f5bdf3bb90fd/torch-2.0.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:25aa43ca80dcdf32f13da04c503ec7afdf8e77e3a0183dd85cd3e53b2842e527", size = 55831241, upload-time = "2023-05-08T17:31:49.294Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-bindings", version = "12.9.4", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "fsspec", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cufile-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-curand-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparselt-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvshmem-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-bindings", version = "13.2.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.15' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "fsspec", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "jinja2", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.11' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version >= '3.11' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.11' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cudnn-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-cusparselt-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nccl-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "nvidia-nvshmem-cu13", marker = "(sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "setuptools", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "sympy", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "triton", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-fmpose3d') or (python_full_version < '3.15' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf') or (python_full_version < '3.15' and sys_platform == 'linux' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version >= '3.15' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "typing-extensions", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/e7/19894fdb51c7dbaf94f5a79bb0871da0992e8e4241e579cb006da46d2e58/torch-2.13.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d", size = 111178962, upload-time = "2026-07-08T16:05:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5c/b1d5de470c54e339b30a92d96683a71bcebd78f5f2a7fc714cd6dc6bbd68/torch-2.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045", size = 427198333, upload-time = "2026-07-08T16:05:36.847Z" }, + { url = "https://files.pythonhosted.org/packages/50/c0/68a84105e1fcb8970144b388ff3d3e5dc15a3be28c1e247841f7d7247e41/torch-2.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4", size = 526555154, upload-time = "2026-07-08T16:05:06.507Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c9/0bb9d097b03cbaf96bb75b15e867347b8e41bfcdfe0539452d17d9e63993/torch-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb", size = 122015602, upload-time = "2026-07-08T16:05:45.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, + { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + +[[package]] +name = "torchvision" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "requests", marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e7/3b43cce519d7236bbbdc31f468b43ae2084ff7db8cb162764311028d32a1/torchvision-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7754088774e810c5672b142a45dcf20b1bd986a5a7da90f8660c43dc43fb850c", size = 1501860, upload-time = "2023-05-08T17:32:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/4cd5133120e6cbcc2fa5c38c92f2f44a7486a9d2ae851e3d5a7e83f396d5/torchvision-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37eb138e13f6212537a3009ac218695483a635c404b6cc1d8e0d0d978026a86d", size = 1410848, upload-time = "2023-05-08T17:34:05.161Z" }, + { url = "https://files.pythonhosted.org/packages/87/0f/88f023bf6176d9af0f85feedf4be129f9cf2748801c4d9c690739a10c100/torchvision-0.15.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:54143f7cc0797d199b98a53b7d21c3f97615762d4dd17ad45a41c7e80d880e73", size = 6032483, upload-time = "2023-05-08T17:33:04.697Z" }, + { url = "https://files.pythonhosted.org/packages/16/5e/51c5fde550161edcfa3e131c51a8b4261775ebb2b118b3560116fa9f7a73/torchvision-0.15.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:1eefebf5fbd01a95fe8f003d623d941601c94b5cec547b420da89cb369d9cf96", size = 1249097, upload-time = "2023-05-08T17:33:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/cb1e7f25b6dda4e672ed8a3e7fbd073ec39e2ba6c378c3071ef2cd6100e1/torchvision-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:96fae30c5ca8423f4b9790df0f0d929748e32718d88709b7b567d2f630c042e3", size = 1195746, upload-time = "2023-05-08T17:33:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/2f3b2392ce7c4b856a5964803c4bc0bf0d5fc75ff7f6cc64cc2058c3e700/torchvision-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f35f6bd5bcc4568e6522e4137fa60fcc72f4fa3e615321c26cd87e855acd398", size = 1501857, upload-time = "2023-05-08T17:33:20.364Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/d713159642b36c42f5b6871330241070797ec89d3f8855eeb91c8baddddd/torchvision-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:757505a0ab2be7096cb9d2bf4723202c971cceddb72c7952a7e877f773de0f8a", size = 1410853, upload-time = "2023-05-08T17:33:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/4b/62/b6ec55347600b02b0a2a6596e673c69424aea7360c48343653866e66aa0d/torchvision-0.15.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:012ad25cfd9019ff9b0714a168727e3845029be1af82296ff1e1482931fa4b80", size = 6032450, upload-time = "2023-05-08T17:33:48.437Z" }, + { url = "https://files.pythonhosted.org/packages/66/e0/cd847d4d22be88a71d5d65f5809342e7ea7ded62230e7bde7420a2105e51/torchvision-0.15.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b02a7ffeaa61448737f39a4210b8ee60234bda0515a0c0d8562f884454105b0f", size = 1249109, upload-time = "2023-05-08T17:33:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/d5/26/a1e128500fb661d3ee7d99b97fb45d3b83e57091278c9babec859da7b87f/torchvision-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:10be76ceded48329d0a0355ac33da131ee3993ff6c125e4a02ab34b5baa2472c", size = 1195759, upload-time = "2023-05-08T17:33:11.008Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-tf-cu12' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, +] + +[[package]] +name = "torchvision" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "pillow", marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-deeplabcut-apple-mchips' or extra == 'extra-10-deeplabcut-fmpose3d' or extra == 'extra-10-deeplabcut-tf' or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/df/1ba039ad6cfe6e69209c36766b9b6e8c6fe92481c6d4e4ca52296f5f699d/torchvision-0.28.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2a1ef4b6f4bf5828b48cfad97372c8982db906830884b2868ba5c3df937a7d81", size = 1856019, upload-time = "2026-07-08T16:07:59.283Z" }, + { url = "https://files.pythonhosted.org/packages/88/ea/5c70ecf86f8e95174a85061cea78683a7bb7f422f09c3f3d4f30b7600fa9/torchvision-0.28.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:546fd85345cf8652f6cd099d4f9884b0ca5c2f3fae78689a21dd2f35ea6b622f", size = 7838211, upload-time = "2026-07-08T16:07:27.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/2f7ff1997d793e45d85fafa8374ee25348b7dae9ac521ba8751d7e1c75d5/torchvision-0.28.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6dfb0f45e2b4ceb4e76f158c3fbb5f44387099f3c466e3423a09ab665a194aba", size = 7669419, upload-time = "2026-07-08T16:07:41.648Z" }, + { url = "https://files.pythonhosted.org/packages/42/d0/2b3c30834ff23acd3854d0ff59bc580711f4b36d725de40105a852ed3719/torchvision-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:7fad44dc9582570c7d92c4487d36ac46998f40cc39b438e8b8f5111a935ce4e8", size = 3500355, upload-time = "2026-07-08T16:07:56.865Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b2/1e010052079e4c577007b789db336ea7075f1a426e84d17121fbc3745516/torchvision-0.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8", size = 1856017, upload-time = "2026-07-08T16:07:55.533Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/1b9c5de9c655ca2df4a74100fa671a7b848532ff787e077ccde14a7dea2a/torchvision-0.28.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c", size = 7841822, upload-time = "2026-07-08T16:07:49.207Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9b/f1e68e861d4462e3e195a642c2b448e7b7d3fad5f209487162b9a2133d9b/torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002", size = 7670718, upload-time = "2026-07-08T16:07:46.525Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/1494610ff54cbb154beb55033cc2cd50f3de04dac132fa2dd00e4f2b2556/torchvision-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37", size = 3814319, upload-time = "2026-07-08T16:07:37.153Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" }, + { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" }, + { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" }, + { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, +] + +[[package]] +name = "triton" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cmake", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "filelock", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "lit", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "torch", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-10-deeplabcut-tf-cu11') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (platform_machine != 'x86_64' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform != 'linux' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/ff6be541195daf77aa5c72303b2354661a69e717967d44d91eb4f3fdce32/triton-2.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505", size = 63268585, upload-time = "2023-03-20T17:32:17.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/4aa0179919306f9c2e3e5308f269d20c094b2a4e2963b656e9405172763f/triton-2.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1", size = 63278135, upload-time = "2023-03-20T17:32:25.22Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ea/629cc37436ca5df93ce98956d09cd2ca1498bfee8ef4972d2fe48b9f958c/triton-3.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3daf64305d6cea88d3334c65ebc9bcd0c64c9564a977084366aa768d57cbcf64", size = 184551013, upload-time = "2026-06-17T20:03:37.551Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/c79c34311625227a288df3e483fc5cdf3d596624cbd4b4758c4cbdc14af3/triton-3.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee89fbf782ec2ad50391dd1cf26cbea4f4467154c37f4773026da8fc31c0f58e", size = 197596267, upload-time = "2026-06-17T19:53:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "vispy" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freetype-py" }, + { name = "hsluv" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2e/ef2697f963111cf1bc83568bbe55f262b7c7c8a72948a6e802a7c236f2c1/vispy-0.15.2.tar.gz", hash = "sha256:d52d10c0697f48990555cea2a2bad3f9f5a772391856fda364ea4bbc69fd075c", size = 2513383, upload-time = "2025-05-19T13:26:41.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/2b/a483bf80575e047173d55f51115c38f9c43962cfd3247a861ce033913bee/vispy-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6bc8a49f4e0b27e19be0da318877666d733e1afc7231407e635f948a8aabb095", size = 1478710, upload-time = "2025-05-19T13:26:00.997Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8c9a3cb402037d6eda521492c04dddf6a00f600c85b650b33342573fe82f/vispy-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89a5d51cc980fed81f16373d515086d5b16da66868df8c1f76e71d4dfc17062c", size = 1472024, upload-time = "2025-05-19T13:26:03.055Z" }, + { url = "https://files.pythonhosted.org/packages/58/67/46111a528d63ad5308e4a547484c75e1c9982ec6a3709732ba0bcd343a7a/vispy-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7cbce48e14e2eeac491688be33b4d422d198515c67a1a87021622790309107e", size = 1845072, upload-time = "2025-05-19T13:26:04.226Z" }, + { url = "https://files.pythonhosted.org/packages/56/91/5c9d739410427c603fd0aa9e6a29b6c3e51edcc7f83f14bf243974808396/vispy-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8b0b6b55a781b6add58f946b95c368bfa203e7915fda234f4993c4db03d3c0", size = 1844568, upload-time = "2025-05-19T13:26:05.511Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8e/f07f9d7a341b5058a99e0700f7cab7d222b159154a392b335f0e59cbf636/vispy-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:0f2639656fd53ee4cbedf3e3caed0cef646f90e7ca4ec777ba18ad247234d8c9", size = 1467701, upload-time = "2025-05-19T13:26:07.193Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e7/3590e46cec1d51a8bba0b2d5442df698aec41ff5757e9e29e04aea3cbe12/vispy-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4f7adab04c0a90ca0a1155f09ecc425ca44bee713e6b2d4970d85fc010b05ee", size = 1478729, upload-time = "2025-05-19T13:26:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/bb/71/5871fe8d2f612502e5e148224426fb263431870a2f5fa5d2205fb9f2606a/vispy-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:725ab77b11bb3a4de5145a25b6e06f4555b0b7b1821baf7ffdd7f674f725dab2", size = 1472015, upload-time = "2025-05-19T13:26:10.246Z" }, + { url = "https://files.pythonhosted.org/packages/67/d8/067de34eac9aecf71a3291c54a738161caa9cbf30d62314b0500abff4117/vispy-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d107b07365bc1bdfa28fe6f8df05004cd73bfd6269230bef26b3862f2016fa64", size = 1872447, upload-time = "2025-05-19T13:26:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/af88bdb15b20a690b3d5409fc89fbdbb79095a593abb8a37094a42428760/vispy-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c67fb06df462b63ba95c77eb00a3d5b7d53e90f69987b0e216ab67ad7d6d00e", size = 1876232, upload-time = "2025-05-19T13:26:13.094Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4d/236c77644db2427f33330fd6372bac416262fd3de6771c555b969f67bd4f/vispy-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:46f8ca742958e19135cfa0f76ef1707666837ae6a9559a3fa3c2a3186967299e", size = 1467601, upload-time = "2025-05-19T13:26:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/31102425ea26bafab27bb5a675b499c57d29e72b1cc25fe3ae8facdd374e/vispy-0.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c8cbc22bc789f238423abadfcc3ba7589b85729be6b179b321720b511012fbee", size = 1478919, upload-time = "2025-05-19T13:26:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/8d/63/a601b8f1e8e418d3821d7b4a465c2eb6695ab8eccadfce886b99ffdfe92a/vispy-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf995b86dcc1eab265fa2bf9a8a5ca5a225b86c3ed4ac63900f0c5e90d5e8100", size = 1471915, upload-time = "2025-05-19T13:26:18.441Z" }, + { url = "https://files.pythonhosted.org/packages/79/1f/ca0deb4a876148c0fa515bf03d03e1dfad4553be8e6f2ab51433c074310b/vispy-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa64fcab1cd1730a10a1c9188e9cef8aefe6d099a46f2f87e2458bcef719918", size = 1866848, upload-time = "2025-05-19T13:26:19.6Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/dc6f335e54877f5d26ad4e4aac8f49b2d6e7719dee3e08f363d882c8aba4/vispy-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d38228cedd23876cb2359ff568d523a97284d225259d450d5e5e2129e98eb1", size = 1870433, upload-time = "2025-05-19T13:26:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/5ffa34d7300c35e8423d51789d594d35eaa7256d210f9909afa9fdd0aa37/vispy-0.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:a84531c1a89f8b9d3992eb0d0ab74f84884f49d1f46a8be3755c809d7507cb01", size = 1468068, upload-time = "2025-05-19T13:26:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/6664b7f0d28e0008a28111bae847a1fa3eedd409458bf01ca45a6e99da6f/vispy-0.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ac46aab46e208b919dcf0bb26869bcd083a0a1a4927bcaf41631ba38c247639", size = 1477880, upload-time = "2025-05-19T13:26:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6f/b9b36f841c5ff7320764f64822e79df3fea8a2c92270cda7f3a634d9a031/vispy-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e197a012503850a77d47177964d572edab964b2a8ea0f9d998c35b81325256c", size = 1471035, upload-time = "2025-05-19T13:26:24.554Z" }, + { url = "https://files.pythonhosted.org/packages/4c/af/b892a3c9b4be29755ce4d1fc17ecb8249446bdd0e17bb5b2a9cb39bcf0f4/vispy-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9eff397ecdbaf0052baae0563caa9e276272d6dc78fbaab3f5e51dbf5f7f92", size = 1860412, upload-time = "2025-05-19T13:26:26.212Z" }, + { url = "https://files.pythonhosted.org/packages/e8/13/8997d96bdc0a81f331dfd41b368935d79e8ea2917840266567e6dc40d684/vispy-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13ee0cb95bbff6d7b235e1a8f96e620eee15ccb6d5ad74902427ab7e56dc8ab", size = 1865860, upload-time = "2025-05-19T13:26:27.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/37/abb30db1853b69aed4c32813cf312f301ec3641b8846193be9a6d892d607/vispy-0.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:f5955821e9b452980490706648a702da8cb2b82e762b6d6589e5872118301b84", size = 1467882, upload-time = "2025-05-19T13:26:29.608Z" }, +] + +[[package]] +name = "wandb" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf", version = "4.25.9", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or extra == 'extra-10-deeplabcut-tf' or extra == 'extra-10-deeplabcut-tf-cu11' or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.13' and sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf-cu12') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version < '3.13' and extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (python_full_version >= '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (python_full_version >= '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest') or (extra != 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "protobuf", version = "6.33.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.13' and sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-fmpose3d' and extra != 'extra-10-deeplabcut-tf') or (python_full_version < '3.13' and extra != 'extra-10-deeplabcut-apple-mchips' and extra != 'extra-10-deeplabcut-tf' and extra != 'extra-10-deeplabcut-tf-cu11' and extra != 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-fmpose3d' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra == 'extra-10-deeplabcut-tf-cu11' and extra == 'extra-10-deeplabcut-tf-latest') or (extra == 'extra-10-deeplabcut-tf-cu12' and extra == 'extra-10-deeplabcut-tf-latest')" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/31/fe53d06b75ef0a7f2f0ee5931a89f7aedc27d233840b1839616860fed256/wandb-0.27.0.tar.gz", hash = "sha256:579e75300173059f9334e1f513a79ef15f6d9ea5c74e20d695633648cdd02031", size = 41090732, upload-time = "2026-05-14T03:44:08.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/5e/2c199e70e636ecfd217cde0bc7469f4511e1d03d0685eb92bfdfce391430/wandb-0.27.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:c156be4851485f3c4160cb6eb2e8991b4cdeffbccefc5636d33cf5e254847365", size = 24886476, upload-time = "2026-05-14T03:43:27.569Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cd/a617c871cd304a9804e56a7ec2ec2c65685bf0091a2b9f91910175a149e2/wandb-0.27.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:20179f38afb0158859a4141d29ac650d3fdbd0cf801a74ce25565c934f03776c", size = 26045779, upload-time = "2026-05-14T03:43:31.999Z" }, + { url = "https://files.pythonhosted.org/packages/10/0a/d3f159a201530b84b72ca5f98c68d1f351c2d9a1864558ed76c811407fae/wandb-0.27.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:626497d7975fa898d0a4a239da7a510483495ca3514510dbe75004a25963af4d", size = 25480764, upload-time = "2026-05-14T03:43:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6a/8721fcdf71d42639191040a77a585d2982402b1754700cb2ecfc2ca1470a/wandb-0.27.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f772da7005cc26a2a32b729a16982a583dc68b3d493df6a09d0aa5c5ca5a2060", size = 27256204, upload-time = "2026-05-14T03:43:39.765Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/279d167ba79fb7a8a43401c9f25efd0f6663ee9bd1eaf5a8578530198888/wandb-0.27.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:63acfc5b994e4a90e4a2fbdee6d45e664da3dd865bb1419942c8995c06c41cf1", size = 25647469, upload-time = "2026-05-14T03:43:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/a69ac59300e3c813939d0764348959ed2a21e14c668cb1cebcb04010da6a/wandb-0.27.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:17aae6e4a88cd05c00ea8f546220918e3ebb6f8c1c36b70ef04a5ac75f0d7160", size = 27599005, upload-time = "2026-05-14T03:43:50.926Z" }, + { url = "https://files.pythonhosted.org/packages/5f/40/bf510c8758727df020f83b717ebc1fcc1739ed7f6ae1796ebef60bf6f592/wandb-0.27.0-py3-none-win32.whl", hash = "sha256:0bd5659417e386bf6538b5e2ffe6885774c6197f0e4853bfed517d5b0db457f1", size = 25036164, upload-time = "2026-05-14T03:43:54.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/ff/69f88e7d90c22b79bcb911143c13e59742ee192080b21015ff83a5a1f60a/wandb-0.27.0-py3-none-win_amd64.whl", hash = "sha256:89d584b73166eecee96fb446f18d0e45b1aa45aba6a3696296f3f06d7454516b", size = 25036170, upload-time = "2026-05-14T03:43:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/38/f7efd7a87297a55c7e9a331a1dbb5b19e54aeacc11fe6f43f8636a73987c/wandb-0.27.0-py3-none-win_arm64.whl", hash = "sha256:a6c129c311edf210a2b4f2f4acc557eff522628125f5f28ed27df19c16c07079", size = 22972710, upload-time = "2026-05-14T03:44:03.275Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu11') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-cu12') or (sys_platform != 'darwin' and extra == 'extra-10-deeplabcut-tf-latest') or (sys_platform == 'darwin' and extra == 'extra-10-deeplabcut-apple-mchips') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu11') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-cu12') or (extra != 'extra-10-deeplabcut-apple-mchips' and extra == 'extra-10-deeplabcut-tf-latest')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + +[[package]] +name = "wrapt" +version = "1.14.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/49/925324e2eaa0c83c45b7a1eb31004af4ad7b37fdc1d9e897ea7d551215da/wrapt-1.14.2.tar.gz", hash = "sha256:19d33781d891c99b5a35f810656d2fe01765b35ccfb6d395007d2edb1058d98f", size = 50701, upload-time = "2025-08-12T06:59:02.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/12f70730cf9237e07aa5887bdb3e3695fd68e7e0d6dd66b5dd7a8794724a/wrapt-1.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:734de7e0182d15f03eb4f4fce0ff7f07a072119a19cdcc97c40aee96d10fe851", size = 35836, upload-time = "2025-08-12T06:58:20.535Z" }, + { url = "https://files.pythonhosted.org/packages/f1/95/5e261ed720bdddf9bbfea2b9bbd30bd03e298b02267333a231592a0cef7b/wrapt-1.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:851ded3662733bab093297a0adb844c71e7754b8c144f76bb569ac6544a7c27a", size = 76907, upload-time = "2025-08-12T06:58:39.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/fcf75886924473ebc11ded6f589f9f715bd605a787201e8b9b1f34b958a7/wrapt-1.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f7b9a0364edef5a3033307cde69f7c3775a74eb83caeff8955e72ec3e0963fe", size = 77665, upload-time = "2025-08-12T06:58:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/16/de/e9205650d0c8401c4e2cc2a01edc89ce3fabda219e2efabcc71d92d02a70/wrapt-1.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3066203bf6e35a040c848143636e6fc2aa4ad6fb2b027a956f462083a3b635ea", size = 76401, upload-time = "2025-08-12T06:58:30.309Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e6/e3b79ba90e7b3a7c1915dbecb84aa491dc0a16238216dae5137bb0d85689/wrapt-1.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:072684f6264ca83695d1c7ad601e61b17953351b0950790cd50c327402006492", size = 76360, upload-time = "2025-08-12T06:58:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/35/07/fe53594e194b133b29b0808c40f1ad92773186c314dbb5b1bf5bb9a4e34a/wrapt-1.14.2-cp310-cp310-win32.whl", hash = "sha256:26c36d922c70de7ae7e933d0f9a10b4017d007d505f60fe02efa8e260a6c7af4", size = 33681, upload-time = "2025-08-12T06:58:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/12/68/134031baba6e1ba7dbdf2a217e99feb7177a2ad860b19f3ba319d40e0baf/wrapt-1.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:ead0c5373df2c551347c7f71903a6403efb391506e1e349704ec21fe797e59bf", size = 35811, upload-time = "2025-08-12T06:58:51.696Z" }, + { url = "https://files.pythonhosted.org/packages/16/ea/7ab441022d98da4cd4ff6d2ec6cbd8e4bf69085f261bdf72971a43bd8a45/wrapt-1.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3913516545546d3ff82ffe5bb4a7eab29dc7f1c71cf7329a1494659dc2a60b7b", size = 35832, upload-time = "2025-08-12T06:58:22.674Z" }, + { url = "https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:122015da6e0a1d09e1f9fbb8c756b9a8802e6cb01138f5256c121602bc7c7399", size = 77384, upload-time = "2025-08-12T06:58:41.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/4057df32059ea4782cf34b7a9dc08c6b29c4e29ecccfa58b32a12eb77582/wrapt-1.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:257a53f69737f340d0ba6e4d5554b56a579ca884de6eada15a7126baae8dacc0", size = 78135, upload-time = "2025-08-12T06:58:31.259Z" }, + { url = "https://files.pythonhosted.org/packages/37/80/d688dfde8c9d75b41c6f01a71e6f64a77861b6ad67e04d622d78060d638c/wrapt-1.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e60ca4aa80a85a0bc24cdf0f971d736f6d0957bdbc93874339027cfb0f286c99", size = 76933, upload-time = "2025-08-12T06:58:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/2b5f8b11c463f75acb6ca710a2ac2e40549f6b5d68bc85c135fc49a7ef19/wrapt-1.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f23bfecdc0e139f15b195ca3601e71a1388dc058bdbbd160ba9d0f4a17b4a12d", size = 76873, upload-time = "2025-08-12T06:58:42.64Z" }, + { url = "https://files.pythonhosted.org/packages/28/d4/003f22d726ca8a0bc2170cad7651dad732f3d3a6e4f69d1f13d8244a93e9/wrapt-1.14.2-cp311-cp311-win32.whl", hash = "sha256:cb5aee915d67441bd07acbce9e4cab1d01a110b4e5f02932e80e5ef4c9411ae2", size = 33675, upload-time = "2025-08-12T06:58:54.885Z" }, + { url = "https://files.pythonhosted.org/packages/8e/61/8eb200c6cf899fedc677a7a01b2f189ad58f1d154d0ef84dab30800b9f01/wrapt-1.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e9773691347205f7d11e7c4395561633285c8d1d691ebfc73ffd64f9072aa22e", size = 35811, upload-time = "2025-08-12T06:58:53.921Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c7/7376998449689cf2adbdbeacad47084410d00f3ae04cf73e6127cf52b950/wrapt-1.14.2-py3-none-any.whl", hash = "sha256:82eea3b559f51f22aefc530b747b87406811ef00cb0c4734f48cb139c748db63", size = 21577, upload-time = "2025-08-12T06:59:01.408Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +]