Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49d5505efa | ||
|
|
1cf95972f3 | ||
|
|
047489b96e | ||
|
|
da498b091b | ||
|
|
2ea0f443f8 | ||
|
|
639318832b | ||
|
|
b9731ef9f3 | ||
|
|
b9475f14f3 | ||
|
|
587c61883b | ||
|
|
f866fe046f | ||
|
|
185322fcd1 | ||
|
|
1dd6ff6ce5 | ||
|
|
9e79cb06d0 | ||
|
|
94131ff56b | ||
|
|
ec0add34ee | ||
|
|
6387513805 | ||
|
|
9ed9cd2592 | ||
|
|
5d6ad66618 | ||
|
|
6f7ca11ea9 | ||
|
|
51a108b5ab | ||
|
|
cfe18afcc9 | ||
|
|
39acd616a6 | ||
|
|
1d0747fbfe | ||
|
|
c3618652cd | ||
|
|
1496888d92 | ||
|
|
058dc169f1 | ||
|
|
0d534346bf | ||
|
|
e73cc438e1 | ||
|
|
6a378a32f7 | ||
|
|
1534501a1d | ||
|
|
941700b9f8 | ||
|
|
4743701286 | ||
|
|
b58862d9f3 | ||
|
|
4b245dd5f1 | ||
|
|
32bf9be5d1 | ||
|
|
f56eb2503e | ||
|
|
b1c0c1b00a | ||
|
|
4c88106d14 | ||
|
|
3d0b4cba65 | ||
|
|
f7fd508400 | ||
|
|
32b10b5335 | ||
|
|
5f981bf4d9 | ||
|
|
5634632944 | ||
|
|
fc62e6a0bc | ||
|
|
837772392c | ||
|
|
386fb2e15f | ||
|
|
ad59b39257 | ||
|
|
57966ce9ec | ||
|
|
ca6609571a | ||
|
|
6690bd678a | ||
|
|
1c489eff98 | ||
|
|
f1e067041e | ||
|
|
73d69172db | ||
|
|
8881b5c588 | ||
|
|
83d9868350 | ||
|
|
c9a3069297 | ||
|
|
8864762ff1 | ||
|
|
39a12db1d4 | ||
|
|
55fafd0a40 | ||
|
|
cc28a51c18 | ||
|
|
9938c1d2ad | ||
|
|
51dba14fee | ||
|
|
78d70085cb | ||
|
|
e2cb82a7da | ||
|
|
78e1c2ceba | ||
|
|
898f2ba72c | ||
|
|
11f2a005e2 | ||
|
|
3ba7c1b8e2 | ||
|
|
3562bbf2ed | ||
|
|
408a9ad0ff | ||
|
|
acf1ef6d90 | ||
|
|
752818e577 | ||
|
|
2e38e8998f | ||
|
|
7878df14be | ||
|
|
dc449c4fff | ||
|
|
afcd0d6e5a | ||
|
|
6b48881de7 | ||
|
|
10beacabc0 | ||
|
|
f180f0f5be | ||
|
|
f906a2978f | ||
|
|
e4949a7de7 | ||
|
|
4e41e9b34f | ||
|
|
e804a01e85 | ||
|
|
d0559e104d | ||
|
|
fa8b6e125c | ||
|
|
47295cd5f7 | ||
|
|
2d48c5dba7 | ||
|
|
13b847d1dc | ||
|
|
7e19c633a6 | ||
|
|
8df41a5683 | ||
|
|
59b652d0f2 | ||
|
|
76429548b6 | ||
|
|
23301bbd17 | ||
|
|
77b7d3733a | ||
|
|
63388fdee9 | ||
|
|
c06d5f0131 | ||
|
|
a151685a57 | ||
|
|
cbaa605e08 | ||
|
|
fb1dbfd084 | ||
|
|
fefd005c2c | ||
|
|
2c53eb10a8 | ||
|
|
d381f43e50 | ||
|
|
dc08d88c5c | ||
|
|
af562950a3 | ||
|
|
ee9568945a | ||
|
|
ea03d55d6a | ||
|
|
3f55c3022e | ||
|
|
a54e491d49 | ||
|
|
4c03f899b9 | ||
|
|
1b5c592abb | ||
|
|
a5cad09782 | ||
|
|
483cc0677f | ||
|
|
b2a1eb5b21 | ||
|
|
fa979cf0ab | ||
|
|
2a1542c465 | ||
|
|
e543aac6dd | ||
|
|
3ee6de3ed3 | ||
|
|
16848e9160 | ||
|
|
071feeadce | ||
|
|
9587439f8d | ||
|
|
a61a6c7144 | ||
|
|
6ae1dfdcb0 | ||
|
|
fc40438f35 | ||
|
|
730bbd246c | ||
|
|
656f639db5 | ||
|
|
663522bed8 | ||
|
|
324872303b | ||
|
|
651ed9d470 | ||
|
|
6aedf4874f | ||
|
|
d33c159a26 | ||
|
|
572ea32c0c | ||
|
|
3415c15365 | ||
|
|
2ed66bfffc | ||
|
|
c6591f609f | ||
|
|
ba7760f9ad | ||
|
|
3f05b107ed | ||
|
|
d4c6fd7426 | ||
|
|
6d8d4fdddb | ||
|
|
adec783d49 | ||
|
|
2cca69be00 | ||
|
|
894ee7f5f9 | ||
|
|
23085eaf8a | ||
|
|
4deb9597b2 | ||
|
|
392d769483 | ||
|
|
f088e16fe2 | ||
|
|
30cf394a7b | ||
|
|
beabf6810c | ||
|
|
dbd6104c8b | ||
|
|
2a5ce95472 | ||
|
|
77ae682120 | ||
|
|
4a0b3bd2cf | ||
|
|
a9aad6727c | ||
|
|
7c6668b58e | ||
|
|
385c26ad78 | ||
|
|
d57078a7ed | ||
|
|
7a5672bbcc | ||
|
|
e6f0a8f3f0 | ||
|
|
9975b63216 | ||
|
|
512edd0c9b | ||
|
|
f4850b870f |
@@ -1,42 +0,0 @@
|
||||
name: 🔧 Poetry Check and Installation Test Workflow
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "poetry.lock"
|
||||
- "pyproject.toml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "poetry.lock"
|
||||
- "pyproject.toml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
poetry-tests:
|
||||
name: Poetry install and check
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
|
||||
- name: 🔍 Check the correctness of the project config
|
||||
run: poetry check
|
||||
|
||||
- name: 🚀 Do Install the package Test
|
||||
run: poetry install --with dev --extras metrics
|
||||
|
||||
- name: 🧪 Run the Import test
|
||||
run: poetry run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
|
||||
@@ -33,17 +33,25 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
- name: 🔑 Create GitHub App token (mkdocs)
|
||||
id: mkdocs_token
|
||||
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.MKDOCS_APP_ID }}
|
||||
private-key: ${{ secrets.MKDOCS_PEM }}
|
||||
owner: roboflow
|
||||
repositories: mkdocs-material-insiders
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
- name: 🏗️ Install dependencies
|
||||
run: |
|
||||
poetry install --with=docs
|
||||
uv pip install -r pyproject.toml --extra docs
|
||||
# Install mkdocs-material-insiders using the GitHub App token
|
||||
uv pip install "git+https://roboflow:${{ steps.mkdocs_token.outputs.token }}@github.com/roboflow/mkdocs-material-insiders.git@9.5.49-insiders-4.53.14#egg=mkdocs-material[imaging]"
|
||||
|
||||
- name: ⚙️ Configure git for github-actions
|
||||
run: |
|
||||
@@ -53,10 +61,10 @@ jobs:
|
||||
- name: 🚀 Deploy Development Docs
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/develop') || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} poetry run mike deploy --push develop
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push develop
|
||||
|
||||
- name: 🚀 Deploy Release Docs
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
run: |
|
||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} poetry run mike deploy --push --update-aliases $latest_tag latest
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push --update-aliases $latest_tag latest
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Publish Supervision Pre-Releases to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-pre-release:
|
||||
name: Publish Pre-release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: test
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
attestations: true
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Publish Supervision Releases to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
name: Publish Release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
attestations: true
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Publish Supervision Pre-Releases to PyPI and TestPyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
build-and-publish-pre-release:
|
||||
name: Publish Pre-releasePackage
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
poetry install --with=build
|
||||
poetry build
|
||||
twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Publish Supervision Releases to TestPyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-testpypi:
|
||||
name: Publish Release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Publish Supervision Releases to PyPI and TestPyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
build-and-publish-pre-release:
|
||||
name: Publish Release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
poetry install --with=build
|
||||
poetry build
|
||||
twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
@@ -23,18 +23,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
|
||||
- name: 🏗️ Install dependencies
|
||||
run: |
|
||||
poetry install --with=docs
|
||||
uv pip install -r pyproject.toml --extra docs
|
||||
|
||||
|
||||
- name: 🧪 Test Docs Build
|
||||
run: |
|
||||
poetry run mkdocs build --verbose
|
||||
run: uv run mkdocs build --verbose
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Test WorkFlow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required for test reporting
|
||||
pull-requests: write # Required for PR comments
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
name: PyTest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📜 Setup Poetry
|
||||
uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd # v3.0.1
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: |
|
||||
poetry install --with=test
|
||||
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@510c5e90c360a185039bea56ce8b3e7e51a16507 # v2.2.0
|
||||
with:
|
||||
verbose: true
|
||||
emoji: true
|
||||
job-summary: true
|
||||
custom-arguments: "-q"
|
||||
click-to-expand: true
|
||||
report-title: "Test Report"
|
||||
custom-pytest: "poetry run pytest"
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 🔧 Pytest/Test Workflow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
name: Import Test and Pytest Run
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🚀 Install Packages
|
||||
run: uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
|
||||
|
||||
- name: 🧪 Run the Import test
|
||||
run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
|
||||
|
||||
- name: 🧪 Run the Test
|
||||
run: uv run pytest
|
||||
@@ -25,14 +25,14 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.8.0'
|
||||
rev: '1.8.3'
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.2
|
||||
rev: v0.11.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
@@ -48,7 +48,7 @@ repos:
|
||||
# args: ["--number"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
|
||||
+6
-17
@@ -155,31 +155,20 @@ Before starting your work on the project, set up your development environment:
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install Poetry:
|
||||
3. Install `uv`:
|
||||
|
||||
Using pip:
|
||||
|
||||
```bash
|
||||
pip install -U pip setuptools
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
Or using pipx (recommended for global installation):
|
||||
|
||||
```bash
|
||||
pipx install poetry
|
||||
```
|
||||
Follow the instructions on the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
4. Install project dependencies:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
|
||||
```
|
||||
|
||||
5. Run pytest to verify the setup:
|
||||
|
||||
```bash
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## 🎨 Code Style and Quality
|
||||
@@ -192,7 +181,7 @@ Furthermore, we have integrated a pre-commit GitHub Action into our workflow. Th
|
||||
|
||||
To run the pre-commit tool, follow these steps:
|
||||
|
||||
1. Install pre-commit by running the following command: `poetry install --with dev`. It will not only install pre-commit but also install all the deps and dev-deps of project
|
||||
1. Install pre-commit by running the following command: `uv pip install -r pyproject.toml --extra dev`. It will not only install pre-commit but also install all the deps and dev-deps of project
|
||||
|
||||
2. Once pre-commit is installed, navigate to the project's root directory.
|
||||
|
||||
@@ -214,7 +203,7 @@ So far, **there is no type checking with mypy**. See [issue](https://github.com/
|
||||
|
||||
The `supervision` documentation is stored in a folder called `docs`. The project documentation is built using `mkdocs`.
|
||||
|
||||
To run the documentation, install the project requirements with `poetry install --with dev`. Then, run `mkdocs serve` to start the documentation server.
|
||||
To run the documentation, install the project requirements with `uv pip install -r pyproject.toml --extra dev --extra docs`. Then, run `mkdocs serve` to start the documentation server.
|
||||
|
||||
You can learn more about mkdocs on the [mkdocs website](https://www.mkdocs.org/).
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ status: new
|
||||
|
||||
:::supervision.detection.utils.xywh_to_xyxy
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xyxy_to_xywh">xyxy_to_xywh</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xyxy_to_xywh
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xyxy_to_xcycarh">xyxy_to_xcycarh</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xyxy_to_xcycarh
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xcycwh_to_xyxy">xcycwh_to_xyxy</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
@@ -77,7 +77,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
@@ -112,7 +112,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
@@ -136,7 +136,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
@@ -168,7 +168,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
@@ -203,7 +203,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
@@ -250,7 +250,7 @@ movement patterns and interactions between objects in the video.
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
@@ -288,7 +288,7 @@ movement patterns and interactions between objects in the video.
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
|
||||
+5
-4
@@ -113,18 +113,19 @@ You can install `supervision` in a
|
||||
pip install -e "."
|
||||
```
|
||||
|
||||
=== "poetry"
|
||||
=== "uv"
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
poetry env use python3.10
|
||||
poetry shell
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# installation
|
||||
poetry install
|
||||
uv pip install -r pyproject.toml -e . --all-extras
|
||||
|
||||
```
|
||||
|
||||
## 🚀 Quickstart
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
!function(){var i="analytics",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","screen","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware","register"];analytics.factory=function(e){return function(){if(window[i].initialized)return window[i][e].apply(window[i],arguments);var n=Array.prototype.slice.call(arguments);if(["track","screen","alias","group","page","identify"].indexOf(e)>-1){var c=document.querySelector("link[rel='canonical']");n.push({__t:"bpc",c:c&&c.getAttribute("href")||void 0,p:location.pathname,u:location.href,s:location.search,t:document.title,r:document.referrer})}n.unshift(e);analytics.push(n);return analytics}};for(var n=0;n<analytics.methods.length;n++){var key=analytics.methods[n];analytics[key]=analytics.factory(key)}analytics.load=function(key,n){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute("data-global-segment-analytics-key",i);t.src="https://cdn.segment.com/analytics.js/v1/" + key + "/analytics.min.js";var r=document.getElementsByTagName("script")[0];r.parentNode.insertBefore(t,r);analytics._loadOptions=n};analytics._writeKey="rMvrPeZBJYyOPJSGCNhMlnTJb8VhFiWU";;analytics.SNIPPET_VERSION="5.2.0";
|
||||
analytics.load("eohFog7VZiAhGJGEr5Sh7BM1mFKmUvDC");
|
||||
document$.subscribe(analytics.page);
|
||||
}}();
|
||||
@@ -35,9 +35,7 @@ def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
|
||||
def initiate_annotators(
|
||||
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator]
|
||||
]:
|
||||
) -> Tuple[List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoxAnnotator]]:
|
||||
line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
|
||||
|
||||
@@ -54,7 +52,7 @@ def initiate_annotators(
|
||||
text_thickness=line_thickness * 2,
|
||||
text_scale=text_scale * 2,
|
||||
)
|
||||
box_annotator = sv.BoundingBoxAnnotator(
|
||||
box_annotator = sv.BoxAnnotator(
|
||||
color=COLORS.by_idx(index), thickness=line_thickness
|
||||
)
|
||||
zones.append(zone)
|
||||
@@ -97,7 +95,7 @@ def annotate(
|
||||
frame: np.ndarray,
|
||||
zones: List[sv.PolygonZone],
|
||||
zone_annotators: List[sv.PolygonZoneAnnotator],
|
||||
box_annotators: List[sv.BoundingBoxAnnotator],
|
||||
box_annotators: List[sv.BoxAnnotator],
|
||||
detections: sv.Detections,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
@@ -108,7 +106,7 @@ def annotate(
|
||||
zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
|
||||
zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
|
||||
drawing zone annotations.
|
||||
box_annotators (List[sv.BoundingBoxAnnotator]): A list of annotators for
|
||||
box_annotators (List[sv.BoxAnnotator]): A list of annotators for
|
||||
drawing box annotations.
|
||||
detections (sv.Detections): Detections to be used for annotation.
|
||||
|
||||
|
||||
@@ -33,9 +33,7 @@ def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
|
||||
def initiate_annotators(
|
||||
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator]
|
||||
]:
|
||||
) -> Tuple[List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoxAnnotator]]:
|
||||
line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
|
||||
|
||||
@@ -52,7 +50,7 @@ def initiate_annotators(
|
||||
text_thickness=line_thickness * 2,
|
||||
text_scale=text_scale * 2,
|
||||
)
|
||||
box_annotator = sv.BoundingBoxAnnotator(
|
||||
box_annotator = sv.BoxAnnotator(
|
||||
color=COLORS.by_idx(index), thickness=line_thickness
|
||||
)
|
||||
zones.append(zone)
|
||||
@@ -94,7 +92,7 @@ def annotate(
|
||||
frame: np.ndarray,
|
||||
zones: List[sv.PolygonZone],
|
||||
zone_annotators: List[sv.PolygonZoneAnnotator],
|
||||
box_annotators: List[sv.BoundingBoxAnnotator],
|
||||
box_annotators: List[sv.BoxAnnotator],
|
||||
detections: sv.Detections,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
@@ -105,7 +103,7 @@ def annotate(
|
||||
zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
|
||||
zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
|
||||
drawing zone annotations.
|
||||
box_annotators (List[sv.BoundingBoxAnnotator]): A list of annotators for
|
||||
box_annotators (List[sv.BoxAnnotator]): A list of annotators for
|
||||
drawing box annotations.
|
||||
detections (sv.Detections): Detections to be used for annotation.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ def process_video(
|
||||
model = get_roboflow_model(model_id=model_id, api_key=roboflow_api_key)
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
|
||||
@@ -16,7 +16,7 @@ def process_video(
|
||||
model = YOLO(source_weights_path)
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
|
||||
+8
-3
@@ -149,9 +149,12 @@ plugins:
|
||||
group_by_category: true
|
||||
docstring_style: google
|
||||
show_symbol_type_heading: true
|
||||
show_root_heading: True
|
||||
show_symbol_type_toc: true
|
||||
show_category_heading: true
|
||||
domains: [std, py]
|
||||
inventories:
|
||||
- url: https://docs.python-requests.org/en/master/objects.inv
|
||||
domains: [std, py]
|
||||
- git-committers:
|
||||
repository: roboflow/supervision
|
||||
branch: develop
|
||||
@@ -176,11 +179,13 @@ markdown_extensions:
|
||||
check_paths: true
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
|
||||
extra_javascript:
|
||||
# disabled until 0.27.0, so we can test its functionality
|
||||
# - "javascripts/init_kapa_widget.js"
|
||||
- "javascripts/init_kapa_widget.js"
|
||||
- "javascripts/cookbooks-card.js"
|
||||
- "javascripts/segment.js"
|
||||
- "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js"
|
||||
|
||||
# Messages shown during document build
|
||||
|
||||
Generated
-5084
File diff suppressed because it is too large
Load Diff
+55
-111
@@ -1,18 +1,16 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "supervision"
|
||||
version = "0.26.0rc2"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
authors = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
|
||||
maintainers = [
|
||||
"Piotr Skalski <piotr.skalski92@gmail.com>",
|
||||
"Linas Kondrackis <linas@roboflow.com>",
|
||||
]
|
||||
license = { text = "MIT" }
|
||||
version = "0.26.0rc6"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
packages = [{ include = "supervision" }, { include = "supervision/py.typed" }]
|
||||
homepage = "https://github.com/roboflow/supervision"
|
||||
repository = "https://github.com/roboflow/supervision"
|
||||
documentation = "https://supervision.roboflow.com/latest/"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" }
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" },
|
||||
]
|
||||
keywords = [
|
||||
"machine-learning",
|
||||
"deep-learning",
|
||||
@@ -22,7 +20,6 @@ keywords = [
|
||||
"AI",
|
||||
"Roboflow",
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
@@ -40,116 +37,59 @@ classifiers = [
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Operating System :: MacOS',
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
|
||||
# For Python versions 3.13 and above, use numpy versions 2.1.0,
|
||||
# required for poetry to not fail.
|
||||
numpy = [
|
||||
{ version = ">=1.21.2", python = "<3.13" },
|
||||
{ version = ">=2.1.0", python = ">=3.13" },
|
||||
dependencies = [
|
||||
"numpy>=1.21.2",
|
||||
"scipy>=1.10.0",
|
||||
"matplotlib>=3.6.0",
|
||||
"pyyaml>=5.3",
|
||||
"defusedxml>=0.7.1",
|
||||
"pillow>=9.4",
|
||||
"requests>=2.26.0",
|
||||
"tqdm>=4.62.3",
|
||||
"opencv-python>=4.5.5.64"
|
||||
]
|
||||
|
||||
scipy = [
|
||||
{ version = "1.10.0", python = "<3.9" },
|
||||
{ version = "^1.10.0", python = ">=3.9" },
|
||||
{ version = ">=1.14.1", python = ">=3.13" },
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/roboflow/supervision"
|
||||
Repository = "https://github.com/roboflow/supervision"
|
||||
Documentation = "https://supervision.roboflow.com/latest/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
metrics = [
|
||||
"pandas>=2.0.0",
|
||||
]
|
||||
|
||||
# Matplotlib sub-dependency
|
||||
# The 'contourpy' package is required by Matplotlib for contour plotting.
|
||||
# We need to ensure compatibility with both Python 3.8 and Python 3.13.
|
||||
#
|
||||
# For Python 3.8 and above, we use version 1.0.7 or higher, as it is the lowest major version that supports Python 3.8.
|
||||
# For Python 3.13 and above, we use version 1.3.0 or higher, as it is the first version that explicitly supports Python 3.13.
|
||||
contourpy = [
|
||||
{ version = ">=1.0.7", python = ">=3.8,<3.13" },
|
||||
{ version = ">=1.3.0", python = ">=3.13" },
|
||||
dev = [
|
||||
"pytest>=7.2.2,<9.0.0",
|
||||
"tox>=4.11.4",
|
||||
"notebook>=6.5.3,<8.0.0",
|
||||
"ipywidgets>=8.1.1",
|
||||
"jupytext>=1.16.1",
|
||||
"nbconvert>=7.14.2",
|
||||
"docutils!=0.21"
|
||||
]
|
||||
|
||||
matplotlib = [
|
||||
{ version = ">=3.6.0,<3.8.0", python = "3.8" },
|
||||
{ version = ">=3.6.0", python = ">=3.9" },
|
||||
{ version = ">=3.7.3", python = ">=3.12" },
|
||||
{ version = ">=3.9.2", python = ">=3.13" },
|
||||
docs = [
|
||||
"mkdocs-material[imaging]>=9.5.5",
|
||||
"mkdocstrings>=0.25.2,<0.30.0",
|
||||
"mkdocstrings-python>=1.10.9",
|
||||
"mike>=2.0.0",
|
||||
"mkdocs-jupyter>=0.24.3",
|
||||
"mkdocs-git-committers-plugin-2>=2.4.1; python_version >= '3.8' and python_version < '4'",
|
||||
"mkdocs-git-revision-date-localized-plugin>=1.2.4"
|
||||
]
|
||||
|
||||
|
||||
pyyaml = ">=5.3"
|
||||
defusedxml = "^0.7.1"
|
||||
pillow = ">=9.4"
|
||||
requests = ">=2.26.0"
|
||||
tqdm = ">=4.62.3"
|
||||
# pandas: picked lowest major version that supports Python 3.8
|
||||
# pandas 2.2.3 has been released with support for Python 3.13
|
||||
pandas = [
|
||||
{ version = ">=2.0.0", python = "<3.13", optional = true },
|
||||
{ version = ">=2.2.3", python = ">=3.13", optional = true },
|
||||
build = [
|
||||
"twine>=5.1.1,<7.0.0",
|
||||
"wheel>=0.40,<0.46",
|
||||
"build>=0.10,<1.3"
|
||||
]
|
||||
|
||||
opencv-python = ">=4.5.5.64"
|
||||
|
||||
[tool.poetry.extras]
|
||||
metrics = ["pandas"]
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = ">=7.2.2,<9.0.0"
|
||||
pytest-md = "^0.2.0"
|
||||
pytest-emoji = "^0.2.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
twine = ">=5.1.1,<7.0.0"
|
||||
pytest = ">=7.2.2,<9.0.0"
|
||||
wheel = ">=0.40,<0.46"
|
||||
build = ">=0.10,<1.3"
|
||||
ruff = ">=0.1.0"
|
||||
mypy = "^1.4.1"
|
||||
pre-commit = "^3.3.3"
|
||||
tox = "^4.11.4"
|
||||
notebook = ">=6.5.3,<8.0.0"
|
||||
ipywidgets = "^8.1.1"
|
||||
jupytext = "^1.16.1"
|
||||
nbconvert = "^7.14.2"
|
||||
docutils = [
|
||||
{ version = "^0.20.1", python = "<3.9" },
|
||||
{ version = "^0.21.1", python = ">=3.9" },
|
||||
]
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = { extras = ["imaging"], version = "^9.5.5" }
|
||||
mkdocstrings = ">=0.25.2,<0.27.0"
|
||||
mkdocstrings-python = "^1.10.9"
|
||||
mike = "^2.0.0"
|
||||
# For Documentation Development use Python 3.10 or above
|
||||
# Use Latest mkdocs-jupyter min 0.24.6 for Jupyter Notebook Theme support
|
||||
mkdocs-jupyter = "^0.24.3"
|
||||
mkdocs-git-committers-plugin-2 = "^2.4.1"
|
||||
mkdocs-git-revision-date-localized-plugin = "^1.2.4"
|
||||
|
||||
[tool.poetry.group.typecheck]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.typecheck.dependencies]
|
||||
types-pyyaml = "^6.0.12.20240808"
|
||||
types-cffi = "^1.16.0.20240331"
|
||||
types-requests = "^2.32.0.20240712"
|
||||
types-tqdm = "^4.66.0.20240417"
|
||||
pandas-stubs = ">=2.0.0.230412"
|
||||
|
||||
[tool.poetry.group.build.dependencies]
|
||||
twine = ">=5.1.1,<7.0.0"
|
||||
|
||||
[tool.bandit]
|
||||
target = ["test", "supervision"]
|
||||
tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412", "B410"]
|
||||
tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412"]
|
||||
|
||||
[tool.autoflake]
|
||||
check = true
|
||||
imports = ["cv2", "supervision"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py38"
|
||||
|
||||
@@ -273,7 +213,7 @@ skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.codespell]
|
||||
skip = "*.ipynb,poetry.lock"
|
||||
skip = "*.ipynb"
|
||||
count = true
|
||||
quiet-level = 3
|
||||
ignore-words-list = "STrack,sTrack,strack"
|
||||
@@ -282,8 +222,12 @@ ignore-words-list = "STrack,sTrack,strack"
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["supervision*"]
|
||||
exclude = ["docs*", "test*", "examples*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
supervision = ["py.typed"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["setuptools >= 61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -9,7 +9,6 @@ except importlib_metadata.PackageNotFoundError:
|
||||
from supervision.annotators.core import (
|
||||
BackgroundOverlayAnnotator,
|
||||
BlurAnnotator,
|
||||
BoundingBoxAnnotator,
|
||||
BoxAnnotator,
|
||||
BoxCornerAnnotator,
|
||||
CircleAnnotator,
|
||||
@@ -46,7 +45,6 @@ from supervision.detection.line_zone import (
|
||||
LineZoneAnnotator,
|
||||
LineZoneAnnotatorMulticlass,
|
||||
)
|
||||
from supervision.detection.lmm import LMM
|
||||
from supervision.detection.overlap_filter import (
|
||||
OverlapFilter,
|
||||
box_non_max_merge,
|
||||
@@ -78,7 +76,10 @@ from supervision.detection.utils import (
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
xyxy_to_polygons,
|
||||
xyxy_to_xcycarh,
|
||||
xyxy_to_xywh,
|
||||
)
|
||||
from supervision.detection.vlm import LMM, VLM
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import (
|
||||
calculate_optimal_line_thickness,
|
||||
@@ -126,7 +127,6 @@ __all__ = [
|
||||
"BackgroundOverlayAnnotator",
|
||||
"BaseDataset",
|
||||
"BlurAnnotator",
|
||||
"BoundingBoxAnnotator",
|
||||
"BoxAnnotator",
|
||||
"BoxCornerAnnotator",
|
||||
"ByteTrack",
|
||||
@@ -226,4 +226,6 @@ __all__ = [
|
||||
"xcycwh_to_xyxy",
|
||||
"xywh_to_xyxy",
|
||||
"xyxy_to_polygons",
|
||||
"xyxy_to_xyah",
|
||||
"xyxy_to_xywh",
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from supervision.annotators.base import BaseAnnotator, ImageType
|
||||
from supervision.annotators.utils import (
|
||||
PENDING_TRACK_ID,
|
||||
ColorLookup,
|
||||
Trace,
|
||||
resolve_color,
|
||||
@@ -36,7 +37,6 @@ from supervision.utils.image import (
|
||||
overlay_image,
|
||||
scale_image,
|
||||
)
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX
|
||||
|
||||
@@ -124,92 +124,6 @@ class BoxAnnotator(BaseAnnotator):
|
||||
return scene
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`BoundingBoxAnnotator` is deprecated and has been renamed to `BoxAnnotator`."
|
||||
" `BoundingBoxAnnotator` will be removed in supervision-0.26.0."
|
||||
)
|
||||
class BoundingBoxAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing bounding boxes on an image using provided detections.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Union[Color, ColorPalette] = ColorPalette.DEFAULT,
|
||||
thickness: int = 2,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color (Union[Color, ColorPalette]): The color or color palette to use for
|
||||
annotating detections.
|
||||
thickness (int): Thickness of the bounding box lines.
|
||||
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
"""
|
||||
self.color: Union[Color, ColorPalette] = color
|
||||
self.thickness: int = thickness
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(
|
||||
self,
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
custom_color_lookup: Optional[np.ndarray] = None,
|
||||
) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with bounding boxes based on the provided detections.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image where bounding boxes will be drawn. `ImageType`
|
||||
is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
||||
detections (Detections): Object detections to annotate.
|
||||
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
|
||||
Allows to override the default color mapping strategy.
|
||||
|
||||
Returns:
|
||||
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
||||
or `PIL.Image.Image`)
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
for detection_idx in range(len(detections)):
|
||||
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
)
|
||||
cv2.rectangle(
|
||||
img=scene,
|
||||
pt1=(x1, y1),
|
||||
pt2=(x2, y2),
|
||||
color=color.as_bgr(),
|
||||
thickness=self.thickness,
|
||||
)
|
||||
return scene
|
||||
|
||||
|
||||
class OrientedBoxAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing oriented bounding boxes on an image using provided detections.
|
||||
@@ -1217,7 +1131,7 @@ class LabelAnnotator(BaseAnnotator):
|
||||
detections: Detections, custom_labels: Optional[List[str]]
|
||||
) -> List[str]:
|
||||
if custom_labels is not None:
|
||||
return custom_labels
|
||||
return [str(label) for label in custom_labels]
|
||||
|
||||
labels = []
|
||||
for idx in range(len(detections)):
|
||||
@@ -1826,6 +1740,7 @@ class TraceAnnotator(BaseAnnotator):
|
||||
"The `tracker_id` field is missing in the provided detections."
|
||||
" See more: https://supervision.roboflow.com/latest/how_to/track_objects"
|
||||
)
|
||||
detections = detections[detections.tracker_id != PENDING_TRACK_ID]
|
||||
|
||||
self.trace.put(detections)
|
||||
for detection_idx in range(len(detections)):
|
||||
@@ -1944,11 +1859,9 @@ class HeatMapAnnotator(BaseAnnotator):
|
||||
temp = temp.astype(np.uint8)
|
||||
if self.kernel_size is not None:
|
||||
temp = cv2.blur(temp, (self.kernel_size, self.kernel_size))
|
||||
hsv = np.zeros(scene.shape)
|
||||
hsv = np.full(scene.shape, 255, dtype=np.uint8)
|
||||
hsv[..., 0] = temp
|
||||
hsv[..., 1] = 255
|
||||
hsv[..., 2] = 255
|
||||
temp = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
|
||||
temp = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
mask = cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0
|
||||
scene[mask] = cv2.addWeighted(temp, self.opacity, scene, 1 - self.opacity, 0)[
|
||||
mask
|
||||
|
||||
@@ -7,6 +7,9 @@ from supervision.detection.core import Detections
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.geometry.core import Position
|
||||
|
||||
PENDING_TRACK_COLOR = Color.GREY
|
||||
PENDING_TRACK_ID = -1
|
||||
|
||||
|
||||
class ColorLookup(Enum):
|
||||
"""
|
||||
@@ -136,6 +139,8 @@ def resolve_color(
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
if color_lookup == ColorLookup.TRACK and idx == PENDING_TRACK_ID:
|
||||
return PENDING_TRACK_COLOR
|
||||
return get_color_by_index(color=color, idx=idx)
|
||||
|
||||
|
||||
|
||||
@@ -705,28 +705,6 @@ class ClassificationDataset(BaseDataset):
|
||||
"a list of paths `List[str]` instead."
|
||||
)
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
"`DetectionDataset.images` property is deprecated and will be removed in "
|
||||
"`supervision-0.26.0`. Iterate with `for path, image, annotation in dataset:` "
|
||||
"instead."
|
||||
)
|
||||
def images(self) -> Dict[str, np.ndarray]:
|
||||
"""
|
||||
Load all images to memory and return them as a dictionary.
|
||||
|
||||
!!! warning
|
||||
|
||||
Only use this when you need all images at once.
|
||||
It is much more memory-efficient to initialize dataset with
|
||||
image paths and use `for path, image, annotation in dataset:`.
|
||||
"""
|
||||
if self._images_in_memory:
|
||||
return self._images_in_memory
|
||||
|
||||
images = {image_path: cv2.imread(image_path) for image_path in self.image_paths}
|
||||
return images
|
||||
|
||||
def _get_image(self, image_path: str) -> np.ndarray:
|
||||
"""Assumes that image is in dataset"""
|
||||
if self._images_in_memory:
|
||||
|
||||
@@ -2,8 +2,8 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
@@ -153,7 +153,18 @@ def load_yolo_annotations(
|
||||
image_paths = [
|
||||
str(path)
|
||||
for path in list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
directory=images_directory_path,
|
||||
extensions=[
|
||||
"bmp",
|
||||
"dng",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"mpo",
|
||||
"png",
|
||||
"tif",
|
||||
"tiff",
|
||||
"webp",
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@@ -167,10 +178,16 @@ def load_yolo_annotations(
|
||||
annotations[image_path] = Detections.empty()
|
||||
continue
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
# PIL is much faster than cv2 for checking image shape and mode: https://github.com/roboflow/supervision/issues/1554
|
||||
image = Image.open(image_path)
|
||||
lines = read_txt_file(file_path=annotation_path, skip_empty=True)
|
||||
h, w, _ = image.shape
|
||||
w, h = image.size
|
||||
resolution_wh = (w, h)
|
||||
if image.mode not in ("RGB", "L"):
|
||||
raise ValueError(
|
||||
f"Images must be 'RGB' or 'grayscale', \
|
||||
but {image_path} mode is '{image.mode}'."
|
||||
)
|
||||
|
||||
with_masks = _with_mask(lines=lines)
|
||||
with_masks = force_masks if force_masks else with_masks
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -9,12 +10,6 @@ from supervision.config import (
|
||||
CLASS_NAME_DATA_FIELD,
|
||||
ORIENTED_BOX_COORDINATES,
|
||||
)
|
||||
from supervision.detection.lmm import (
|
||||
LMM,
|
||||
from_florence_2,
|
||||
from_paligemma,
|
||||
validate_lmm_parameters,
|
||||
)
|
||||
from supervision.detection.overlap_filter import (
|
||||
box_non_max_merge,
|
||||
box_non_max_suppression,
|
||||
@@ -38,8 +33,16 @@ from supervision.detection.utils import (
|
||||
process_roboflow_result,
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
from supervision.detection.vlm import (
|
||||
LMM,
|
||||
VLM,
|
||||
from_florence_2,
|
||||
from_paligemma,
|
||||
from_qwen_2_5_vl,
|
||||
validate_vlm_parameters,
|
||||
)
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.utils.internal import get_instance_variables
|
||||
from supervision.utils.internal import deprecated, get_instance_variables
|
||||
from supervision.validators import validate_detections_fields
|
||||
|
||||
|
||||
@@ -712,7 +715,7 @@ class Detections:
|
||||
"""
|
||||
if "error" in azure_result:
|
||||
raise ValueError(
|
||||
f'Azure API returned an error {azure_result["error"]["message"]}'
|
||||
f"Azure API returned an error {azure_result['error']['message']}"
|
||||
)
|
||||
|
||||
xyxy, confidences, class_ids = [], [], []
|
||||
@@ -799,6 +802,10 @@ class Detections:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Detections.from_lmm` property is deprecated and will be removed in "
|
||||
"`supervision-0.31.0`. Use Detections.from_vlm instead."
|
||||
)
|
||||
def from_lmm(
|
||||
cls, lmm: Union[LMM, str], result: Union[str, dict], **kwargs: Any
|
||||
) -> Detections:
|
||||
@@ -837,16 +844,51 @@ class Detections:
|
||||
# array([0])
|
||||
```
|
||||
"""
|
||||
lmm = validate_lmm_parameters(lmm, result, kwargs)
|
||||
# filler logic mapping old from_lmm to new from_vlm
|
||||
lmm_to_vlm = {
|
||||
LMM.PALIGEMMA: VLM.PALIGEMMA,
|
||||
LMM.FLORENCE_2: VLM.FLORENCE_2,
|
||||
LMM.QWEN_2_5_VL: VLM.QWEN_2_5_VL,
|
||||
}
|
||||
|
||||
if lmm == LMM.PALIGEMMA:
|
||||
assert isinstance(result, str)
|
||||
# (this works even if the LMM enum is wrapped by @deprecated)
|
||||
if isinstance(lmm, Enum) and lmm.__class__.__name__ == "LMM":
|
||||
vlm = lmm_to_vlm[lmm]
|
||||
|
||||
elif isinstance(lmm, str):
|
||||
try:
|
||||
lmm_enum = LMM(lmm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid LMM string '{lmm}'. Must be one of "
|
||||
f"{[m.value for m in LMM]}"
|
||||
)
|
||||
vlm = lmm_to_vlm[lmm_enum]
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid type for 'lmm': {type(lmm)}. Must be LMM or str."
|
||||
)
|
||||
|
||||
return cls.from_vlm(vlm=vlm, result=result, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_vlm(
|
||||
cls, vlm: Union[VLM, str], result: Union[str, dict], **kwargs: Any
|
||||
) -> Detections:
|
||||
vlm = validate_vlm_parameters(vlm, result, kwargs)
|
||||
|
||||
if vlm == VLM.PALIGEMMA:
|
||||
xyxy, class_id, class_name = from_paligemma(result, **kwargs)
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if lmm == LMM.FLORENCE_2:
|
||||
assert isinstance(result, dict)
|
||||
if vlm == VLM.QWEN_2_5_VL:
|
||||
xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs)
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if vlm == VLM.FLORENCE_2:
|
||||
xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs)
|
||||
if len(xyxy) == 0:
|
||||
return cls.empty()
|
||||
@@ -859,8 +901,6 @@ class Detections:
|
||||
|
||||
return cls(xyxy=xyxy, mask=mask, data=data)
|
||||
|
||||
raise ValueError(f"Unsupported LMM: {lmm}")
|
||||
|
||||
@classmethod
|
||||
def from_easyocr(cls, easyocr_results: list) -> Detections:
|
||||
"""
|
||||
@@ -1027,16 +1067,16 @@ class Detections:
|
||||
detections_1 = sv.Detections(
|
||||
xyxy=np.array([[15, 15, 100, 100], [200, 200, 300, 300]]),
|
||||
class_id=np.array([1, 2]),
|
||||
data={'feature_vector': np.array([0.1, 0.2)])}
|
||||
)
|
||||
data={'feature_vector': np.array([0.1, 0.2])}
|
||||
)
|
||||
|
||||
detections_2 = sv.Detections(
|
||||
xyxy=np.array([[30, 30, 120, 120]]),
|
||||
class_id=np.array([1]),
|
||||
data={'feature_vector': [np.array([0.3])]}
|
||||
)
|
||||
data={'feature_vector': np.array([0.3])}
|
||||
)
|
||||
|
||||
merged_detections = Detections.merge([detections_1, detections_2])
|
||||
merged_detections = sv.Detections.merge([detections_1, detections_2])
|
||||
|
||||
merged_detections.xyxy
|
||||
array([[ 15, 15, 100, 100],
|
||||
@@ -1203,7 +1243,7 @@ class Detections:
|
||||
if isinstance(index, str):
|
||||
return self.data.get(index)
|
||||
if self.is_empty():
|
||||
return Detections.empty()
|
||||
return self
|
||||
if isinstance(index, int):
|
||||
index = [index]
|
||||
return Detections(
|
||||
@@ -1306,9 +1346,9 @@ class Detections:
|
||||
if len(self) == 0:
|
||||
return self
|
||||
|
||||
assert (
|
||||
self.confidence is not None
|
||||
), "Detections confidence must be given for NMS to be executed."
|
||||
assert self.confidence is not None, (
|
||||
"Detections confidence must be given for NMS to be executed."
|
||||
)
|
||||
|
||||
if class_agnostic:
|
||||
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
|
||||
@@ -1362,9 +1402,9 @@ class Detections:
|
||||
if len(self) == 0:
|
||||
return self
|
||||
|
||||
assert (
|
||||
self.confidence is not None
|
||||
), "Detections confidence must be given for NMM to be executed."
|
||||
assert self.confidence is not None, (
|
||||
"Detections confidence must be given for NMM to be executed."
|
||||
)
|
||||
|
||||
if class_agnostic:
|
||||
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
|
||||
|
||||
class LMM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
|
||||
|
||||
RESULT_TYPES: Dict[LMM, type] = {LMM.PALIGEMMA: str, LMM.FLORENCE_2: dict}
|
||||
|
||||
REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {
|
||||
LMM.PALIGEMMA: ["resolution_wh"],
|
||||
LMM.FLORENCE_2: ["resolution_wh"],
|
||||
}
|
||||
|
||||
ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {
|
||||
LMM.PALIGEMMA: ["resolution_wh", "classes"],
|
||||
LMM.FLORENCE_2: ["resolution_wh"],
|
||||
}
|
||||
|
||||
SUPPORTED_TASKS_FLORENCE_2 = [
|
||||
"<OD>",
|
||||
"<CAPTION_TO_PHRASE_GROUNDING>",
|
||||
"<DENSE_REGION_CAPTION>",
|
||||
"<REGION_PROPOSAL>",
|
||||
"<OCR_WITH_REGION>",
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>",
|
||||
"<REGION_TO_SEGMENTATION>",
|
||||
"<OPEN_VOCABULARY_DETECTION>",
|
||||
"<REGION_TO_CATEGORY>",
|
||||
"<REGION_TO_DESCRIPTION>",
|
||||
]
|
||||
|
||||
|
||||
def validate_lmm_parameters(
|
||||
lmm: Union[LMM, str], result: Any, kwargs: Dict[str, Any]
|
||||
) -> LMM:
|
||||
if isinstance(lmm, str):
|
||||
try:
|
||||
lmm = LMM(lmm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid lmm value: {lmm}. Must be one of {[e.value for e in LMM]}"
|
||||
)
|
||||
|
||||
if not isinstance(result, RESULT_TYPES[lmm]):
|
||||
raise ValueError(
|
||||
f"Invalid LMM result type: {type(result)}. Must be {RESULT_TYPES[lmm]}"
|
||||
)
|
||||
|
||||
required_args = REQUIRED_ARGUMENTS.get(lmm, [])
|
||||
for arg in required_args:
|
||||
if arg not in kwargs:
|
||||
raise ValueError(f"Missing required argument: {arg}")
|
||||
|
||||
allowed_args = ALLOWED_ARGUMENTS.get(lmm, [])
|
||||
for arg in kwargs:
|
||||
if arg not in allowed_args:
|
||||
raise ValueError(f"Argument {arg} is not allowed for {lmm.name}")
|
||||
|
||||
return lmm
|
||||
|
||||
|
||||
def from_paligemma(
|
||||
result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
w, h = resolution_wh
|
||||
pattern = re.compile(
|
||||
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
|
||||
)
|
||||
matches = pattern.findall(result)
|
||||
matches = np.array(matches) if matches else np.empty((0, 5))
|
||||
|
||||
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
|
||||
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(class_name.astype(str))
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name]).astype(bool)
|
||||
xyxy, class_name = xyxy[mask], class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_florence_2(
|
||||
result: dict, resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]
|
||||
]:
|
||||
"""
|
||||
Parse results from the Florence 2 multi-model model.
|
||||
https://huggingface.co/microsoft/Florence-2-large
|
||||
|
||||
Parameters:
|
||||
result: dict containing the model output
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
labels: (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
masks: (Optional[np.ndarray]): An array of shape `(n, h, w)` containing
|
||||
the segmentation masks for each bounding box
|
||||
obb_boxes: (Optional[np.ndarray]): An array of shape `(n, 4, 2)` containing
|
||||
oriented bounding boxes.
|
||||
"""
|
||||
assert len(result) == 1, f"Expected result with a single element. Got: {result}"
|
||||
task = next(iter(result.keys()))
|
||||
if task not in SUPPORTED_TASKS_FLORENCE_2:
|
||||
raise ValueError(
|
||||
f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}"
|
||||
)
|
||||
result = result[task]
|
||||
|
||||
if task in ["<OD>", "<CAPTION_TO_PHRASE_GROUNDING>", "<DENSE_REGION_CAPTION>"]:
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task == "<REGION_PROPOSAL>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
# provides labels, but they are ["", "", "", ...]
|
||||
return xyxy, None, None, None
|
||||
|
||||
if task == "<OCR_WITH_REGION>":
|
||||
xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32)
|
||||
xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy])
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
masks = np.array(masks_list)
|
||||
return xyxy, None, masks, None
|
||||
|
||||
if task == "<OPEN_VOCABULARY_DETECTION>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["bboxes_labels"])
|
||||
# Also has "polygons" and "polygons_labels", but they don't seem to be used
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task in ["<REGION_TO_CATEGORY>", "<REGION_TO_DESCRIPTION>"]:
|
||||
assert isinstance(
|
||||
result, str
|
||||
), f"Expected string as <REGION_TO_CATEGORY> result, got {type(result)}"
|
||||
|
||||
if result == "No object detected.":
|
||||
return np.empty((0, 4), dtype=np.float32), np.array([]), None, None
|
||||
|
||||
pattern = re.compile(r"<loc_(\d+)><loc_(\d+)><loc_(\d+)><loc_(\d+)>")
|
||||
match = pattern.search(result)
|
||||
assert (
|
||||
match is not None
|
||||
), f"Expected string to end in location tags, but got {result}"
|
||||
|
||||
w, h = resolution_wh
|
||||
xyxy = np.array([match.groups()], dtype=np.float32)
|
||||
xyxy *= np.array([w, h, w, h]) / 1000
|
||||
result_string = result[: match.start()]
|
||||
labels = np.array([result_string])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
assert False, f"Unimplemented task: {task}"
|
||||
@@ -226,8 +226,7 @@ def box_non_max_merge(
|
||||
for merge_group in merge_groups:
|
||||
if len(merge_group) == 0:
|
||||
raise ValueError(
|
||||
f"Empty group detected when non-max-merging "
|
||||
f"detections: {merge_groups}"
|
||||
f"Empty group detected when non-max-merging detections: {merge_groups}"
|
||||
)
|
||||
return merge_groups
|
||||
|
||||
|
||||
@@ -321,6 +321,43 @@ def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray:
|
||||
return xyxy
|
||||
|
||||
|
||||
def xyxy_to_xywh(xyxy: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)`
|
||||
format to `(x, y, width, height)` format.
|
||||
|
||||
Args:
|
||||
xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each row
|
||||
corresponds to a bounding box in the format `(x_min, y_min, x_max,
|
||||
y_max)`.
|
||||
|
||||
Returns:
|
||||
np.ndarray: A numpy array of shape `(N, 4)` where each row corresponds
|
||||
to a bounding box in the format `(x, y, width, height)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xyxy = np.array([
|
||||
[10, 20, 40, 60],
|
||||
[15, 25, 50, 70]
|
||||
])
|
||||
|
||||
sv.xyxy_to_xywh(xyxy=xyxy)
|
||||
# array([
|
||||
# [10, 20, 30, 40],
|
||||
# [15, 25, 35, 45]
|
||||
# ])
|
||||
```
|
||||
"""
|
||||
xywh = xyxy.copy()
|
||||
xywh[:, 2] = xyxy[:, 2] - xyxy[:, 0]
|
||||
xywh[:, 3] = xyxy[:, 3] - xyxy[:, 1]
|
||||
return xywh
|
||||
|
||||
|
||||
def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(center_x, center_y, width, height)`
|
||||
@@ -360,6 +397,56 @@ def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray:
|
||||
return xyxy
|
||||
|
||||
|
||||
def xyxy_to_xcycarh(xyxy: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)`
|
||||
into measurement space to format `(center x, center y, aspect ratio, height)`,
|
||||
where the aspect ratio is `width / height`.
|
||||
|
||||
Args:
|
||||
xyxy (np.ndarray): Bounding box in format `(x1, y1, x2, y2)`.
|
||||
Expected shape is `(N, 4)`.
|
||||
Returns:
|
||||
np.ndarray: Bounding box in format
|
||||
`(center x, center y, aspect ratio, height)`. Shape `(N, 4)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xyxy = np.array([
|
||||
[10, 20, 40, 60],
|
||||
[15, 25, 50, 70]
|
||||
])
|
||||
|
||||
sv.xyxy_to_xcycarh(xyxy=xyxy)
|
||||
# array([
|
||||
# [25. , 40. , 0.75, 40. ],
|
||||
# [32.5 , 47.5 , 0.77777778, 45. ]
|
||||
# ])
|
||||
```
|
||||
|
||||
"""
|
||||
if xyxy.size == 0:
|
||||
return np.empty((0, 4), dtype=float)
|
||||
|
||||
x1, y1, x2, y2 = xyxy.T
|
||||
width = x2 - x1
|
||||
height = y2 - y1
|
||||
center_x = x1 + width / 2
|
||||
center_y = y1 + height / 2
|
||||
|
||||
aspect_ratio = np.divide(
|
||||
width,
|
||||
height,
|
||||
out=np.zeros_like(width, dtype=float),
|
||||
where=height != 0,
|
||||
)
|
||||
result = np.column_stack((center_x, center_y, aspect_ratio, height))
|
||||
return result.astype(float)
|
||||
|
||||
|
||||
def mask_to_xyxy(masks: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts a 3D `np.array` of 2D bool masks into a 2D `np.array` of bounding boxes.
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import json
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`LMM` enum is deprecated and will be removed in "
|
||||
"`supervision-0.31.0`. Use VLM instead."
|
||||
)
|
||||
class LMM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
|
||||
|
||||
class VLM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
|
||||
|
||||
RESULT_TYPES: Dict[VLM, type] = {
|
||||
VLM.PALIGEMMA: str,
|
||||
VLM.FLORENCE_2: dict,
|
||||
VLM.QWEN_2_5_VL: str,
|
||||
}
|
||||
|
||||
REQUIRED_ARGUMENTS: Dict[VLM, List[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh"],
|
||||
}
|
||||
|
||||
ALLOWED_ARGUMENTS: Dict[VLM, List[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh", "classes"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh", "classes"],
|
||||
}
|
||||
|
||||
SUPPORTED_TASKS_FLORENCE_2 = [
|
||||
"<OD>",
|
||||
"<CAPTION_TO_PHRASE_GROUNDING>",
|
||||
"<DENSE_REGION_CAPTION>",
|
||||
"<REGION_PROPOSAL>",
|
||||
"<OCR_WITH_REGION>",
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>",
|
||||
"<REGION_TO_SEGMENTATION>",
|
||||
"<OPEN_VOCABULARY_DETECTION>",
|
||||
"<REGION_TO_CATEGORY>",
|
||||
"<REGION_TO_DESCRIPTION>",
|
||||
]
|
||||
|
||||
|
||||
def validate_vlm_parameters(
|
||||
vlm: Union[VLM, str], result: Any, kwargs: Dict[str, Any]
|
||||
) -> VLM:
|
||||
if isinstance(vlm, str):
|
||||
try:
|
||||
vlm = VLM(vlm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid vlm value: {vlm}. Must be one of {[e.value for e in VLM]}"
|
||||
)
|
||||
|
||||
if not isinstance(result, RESULT_TYPES[vlm]):
|
||||
raise ValueError(
|
||||
f"Invalid VLM result type: {type(result)}. Must be {RESULT_TYPES[vlm]}"
|
||||
)
|
||||
|
||||
required_args = REQUIRED_ARGUMENTS.get(vlm, [])
|
||||
for arg in required_args:
|
||||
if arg not in kwargs:
|
||||
raise ValueError(f"Missing required argument: {arg}")
|
||||
|
||||
allowed_args = ALLOWED_ARGUMENTS.get(vlm, [])
|
||||
for arg in kwargs:
|
||||
if arg not in allowed_args:
|
||||
raise ValueError(f"Argument {arg} is not allowed for {vlm.name}")
|
||||
|
||||
return vlm
|
||||
|
||||
|
||||
def from_paligemma(
|
||||
result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
"""
|
||||
Parse bounding boxes from paligemma-formatted text, scale them to the specified
|
||||
resolution, and optionally filter by classes.
|
||||
|
||||
Args:
|
||||
result: String containing paligemma-formatted locations and labels.
|
||||
resolution_wh: Tuple (width, height) to which we scale the box coordinates.
|
||||
classes: Optional list of valid class names. If provided, boxes and labels not
|
||||
in this list are filtered out.
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`.
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class indices for each bounding box (or `None` if classes is not
|
||||
provided).
|
||||
class_name (np.ndarray): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box.
|
||||
"""
|
||||
|
||||
w, h = resolution_wh
|
||||
if w <= 0 or h <= 0:
|
||||
raise ValueError(
|
||||
f"Both dimensions in resolution_wh must be positive. Got ({w}, {h})."
|
||||
)
|
||||
|
||||
pattern = re.compile(
|
||||
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
|
||||
)
|
||||
matches = pattern.findall(result)
|
||||
matches = np.array(matches) if matches else np.empty((0, 5))
|
||||
|
||||
if matches.shape[0] == 0:
|
||||
return np.empty((0, 4)), None, np.empty(0, dtype=str)
|
||||
|
||||
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
|
||||
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(class_name.astype(str))
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_qwen_2_5_vl(
|
||||
result: str,
|
||||
input_wh: Tuple[int, int],
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]] = None,
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
"""
|
||||
Parse and scale bounding boxes from Qwen-2.5-VL style JSON output.
|
||||
|
||||
The JSON is expected to be enclosed in triple backticks with the format:
|
||||
```json
|
||||
[
|
||||
{"bbox_2d": [x1, y1, x2, y2], "label": "some class name"},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Args:
|
||||
result: String containing the JSON snippet enclosed by triple backticks.
|
||||
input_wh: (input_width, input_height) describing the original bounding box
|
||||
scale.
|
||||
resolution_wh: (output_width, output_height) to which we rescale the boxes.
|
||||
classes: Optional list of valid class names. If provided, returned boxes/labels
|
||||
are filtered to only those classes found here.
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class indices for each bounding box (or None if `classes` is not
|
||||
provided)
|
||||
class_name (np.ndarray): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
"""
|
||||
in_w, in_h = input_wh
|
||||
out_w, out_h = resolution_wh
|
||||
|
||||
if in_w <= 0 or in_h <= 0 or out_w <= 0 or out_h <= 0:
|
||||
raise ValueError(
|
||||
f"Both input and resolution dimensions must be positive. "
|
||||
f"Got input_wh=({in_w}, {in_h}), resolution_wh=({out_w}, {out_h})."
|
||||
)
|
||||
|
||||
pattern = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
|
||||
|
||||
match = pattern.search(result)
|
||||
if not match:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
json_snippet = match.group(1)
|
||||
|
||||
try:
|
||||
data = json.loads(json_snippet)
|
||||
except json.JSONDecodeError:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
boxes_list = []
|
||||
labels_list = []
|
||||
|
||||
for item in data:
|
||||
if "bbox_2d" not in item or "label" not in item:
|
||||
continue
|
||||
boxes_list.append(item["bbox_2d"])
|
||||
labels_list.append(item["label"])
|
||||
|
||||
if not boxes_list:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
xyxy = np.array(boxes_list, dtype=float)
|
||||
class_name = np.array(labels_list, dtype=str)
|
||||
|
||||
xyxy = xyxy / [in_w, in_h, in_w, in_h]
|
||||
xyxy = xyxy * [out_w, out_h, out_w, out_h]
|
||||
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([label in classes for label in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(label) for label in class_name], dtype=int)
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_florence_2(
|
||||
result: dict, resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]
|
||||
]:
|
||||
"""
|
||||
Parse results from the Florence 2 multi-model model.
|
||||
https://huggingface.co/microsoft/Florence-2-large
|
||||
|
||||
Parameters:
|
||||
result: dict containing the model output
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
labels: (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
masks: (Optional[np.ndarray]): An array of shape `(n, h, w)` containing
|
||||
the segmentation masks for each bounding box
|
||||
obb_boxes: (Optional[np.ndarray]): An array of shape `(n, 4, 2)` containing
|
||||
oriented bounding boxes.
|
||||
"""
|
||||
assert len(result) == 1, f"Expected result with a single element. Got: {result}"
|
||||
task = next(iter(result.keys()))
|
||||
if task not in SUPPORTED_TASKS_FLORENCE_2:
|
||||
raise ValueError(
|
||||
f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}"
|
||||
)
|
||||
result = result[task]
|
||||
|
||||
if task in ["<OD>", "<CAPTION_TO_PHRASE_GROUNDING>", "<DENSE_REGION_CAPTION>"]:
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task == "<REGION_PROPOSAL>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
# provides labels, but they are ["", "", "", ...]
|
||||
return xyxy, None, None, None
|
||||
|
||||
if task == "<OCR_WITH_REGION>":
|
||||
xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32)
|
||||
xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy])
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
masks = np.array(masks_list)
|
||||
return xyxy, None, masks, None
|
||||
|
||||
if task == "<OPEN_VOCABULARY_DETECTION>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["bboxes_labels"])
|
||||
# Also has "polygons" and "polygons_labels", but they don't seem to be used
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task in ["<REGION_TO_CATEGORY>", "<REGION_TO_DESCRIPTION>"]:
|
||||
assert isinstance(result, str), (
|
||||
f"Expected string as <REGION_TO_CATEGORY> result, got {type(result)}"
|
||||
)
|
||||
|
||||
if result == "No object detected.":
|
||||
return np.empty((0, 4), dtype=np.float32), np.array([]), None, None
|
||||
|
||||
pattern = re.compile(r"<loc_(\d+)><loc_(\d+)><loc_(\d+)><loc_(\d+)>")
|
||||
match = pattern.search(result)
|
||||
assert match is not None, (
|
||||
f"Expected string to end in location tags, but got {result}"
|
||||
)
|
||||
|
||||
w, h = resolution_wh
|
||||
xyxy = np.array([match.groups()], dtype=np.float32)
|
||||
xyxy *= np.array([w, h, w, h]) / 1000
|
||||
result_string = result[: match.start()]
|
||||
labels = np.array([result_string])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
assert False, f"Unimplemented task: {task}"
|
||||
@@ -83,15 +83,16 @@ class Color:
|
||||
# Color(r=255, g=255, b=255)
|
||||
```
|
||||
|
||||
| Constant | Hex Code | RGB |
|
||||
|------------|------------|------------------|
|
||||
| `WHITE` | `#FFFFFF` | `(255, 255, 255)`|
|
||||
| `BLACK` | `#000000` | `(0, 0, 0)` |
|
||||
| `RED` | `#FF0000` | `(255, 0, 0)` |
|
||||
| `GREEN` | `#00FF00` | `(0, 255, 0)` |
|
||||
| `BLUE` | `#0000FF` | `(0, 0, 255)` |
|
||||
| `YELLOW` | `#FFFF00` | `(255, 255, 0)` |
|
||||
| `ROBOFLOW` | `#A351FB` | `(163, 81, 251)` |
|
||||
| Constant | Hex Code | RGB |
|
||||
|------------|------------|-------------------|
|
||||
| `WHITE` | `#FFFFFF` | `(255, 255, 255)` |
|
||||
| `BLACK` | `#000000` | `(0, 0, 0)` |
|
||||
| `GREY` | `#808080` | `(128, 128, 128)` |
|
||||
| `RED` | `#FF0000` | `(255, 0, 0)` |
|
||||
| `GREEN` | `#00FF00` | `(0, 255, 0)` |
|
||||
| `BLUE` | `#0000FF` | `(0, 0, 255)` |
|
||||
| `YELLOW` | `#FFFF00` | `(255, 255, 0)` |
|
||||
| `ROBOFLOW` | `#A351FB` | `(163, 81, 251)` |
|
||||
"""
|
||||
|
||||
r: int
|
||||
@@ -235,6 +236,10 @@ class Color:
|
||||
def BLACK(cls) -> Color:
|
||||
return Color.from_hex("#000000")
|
||||
|
||||
@classproperty
|
||||
def GREY(cls) -> Color:
|
||||
return Color.from_hex("#808080")
|
||||
|
||||
@classproperty
|
||||
def RED(cls) -> Color:
|
||||
return Color.from_hex("#FF0000")
|
||||
|
||||
@@ -23,8 +23,8 @@ class KeyPoints:
|
||||
=== "Ultralytics"
|
||||
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics)
|
||||
pose result.
|
||||
method, which accepts [YOLOv8-pose](https://docs.ultralytics.com/models/yolov8/), [YOLO11-pose](https://docs.ultralytics.com/models/yolo11/)
|
||||
[pose](https://docs.ultralytics.com/tasks/pose/) result.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
@@ -32,7 +32,7 @@ class KeyPoints:
|
||||
from ultralytics import YOLO
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = YOLO('yolov8s-pose.pt')
|
||||
model = YOLO('yolo11s-pose.pt')
|
||||
|
||||
result = model(image)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(result)
|
||||
@@ -61,6 +61,7 @@ class KeyPoints:
|
||||
method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe)
|
||||
pose result.
|
||||
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
@@ -91,10 +92,10 @@ class KeyPoints:
|
||||
xy (np.ndarray): An array of shape `(n, m, 2)` containing
|
||||
`n` detected objects, each composed of `m` equally-sized
|
||||
sets of keypoints, where each point is `[x, y]`.
|
||||
confidence (Optional[np.ndarray]): An array of shape
|
||||
`(n, m)` containing the confidence scores of each keypoint.
|
||||
class_id (Optional[np.ndarray]): An array of shape
|
||||
`(n,)` containing the class ids of the detected objects.
|
||||
confidence (Optional[np.ndarray]): An array of shape
|
||||
`(n, m)` containing the confidence scores of each keypoint.
|
||||
data (Dict[str, Union[np.ndarray, List]]): A dictionary containing additional
|
||||
data where each key is a string representing the data type, and the value
|
||||
is either a NumPy array or a list of corresponding data of length `n`
|
||||
@@ -314,6 +315,7 @@ class KeyPoints:
|
||||
key_points = sv.KeyPoints.from_mediapipe(
|
||||
face_landmarker_result, (image_width, image_height))
|
||||
```
|
||||
|
||||
""" # noqa: E501 // docs
|
||||
if hasattr(mediapipe_results, "pose_landmarks"):
|
||||
results = mediapipe_results.pose_landmarks
|
||||
@@ -473,7 +475,7 @@ class KeyPoints:
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates, class IDs,
|
||||
and class names, and confidences of each keypoint.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -510,6 +512,91 @@ class KeyPoints:
|
||||
else:
|
||||
return cls.empty()
|
||||
|
||||
@classmethod
|
||||
def from_transformers(cls, transfomers_results: Any) -> KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the
|
||||
[Transformers](https://github.com/huggingface/transformers) inference result.
|
||||
|
||||
Args:
|
||||
transfomers_results (Any): The output of a
|
||||
Transformers model containing instances with prediction data.
|
||||
|
||||
Returns:
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates, class IDs,
|
||||
and class names, and confidences of each keypoint.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from PIL import Image
|
||||
import requests
|
||||
import supervision as sv
|
||||
import torch
|
||||
from transformers import (
|
||||
AutoProcessor,
|
||||
RTDetrForObjectDetection,
|
||||
VitPoseForPoseEstimation,
|
||||
)
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
image = Image.open(<SOURCE_IMAGE_PATH>)
|
||||
|
||||
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
|
||||
|
||||
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
|
||||
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = detection_model(**inputs)
|
||||
|
||||
target_size = torch.tensor([(frame.height, frame.width)])
|
||||
results = detection_processor.post_process_object_detection(
|
||||
outputs, target_sizes=target_size, threshold=0.3)
|
||||
|
||||
detections = sv.Detections.from_transformers(results[0])
|
||||
boxes = sv.xyxy_to_xywh(detections[detections.class_id == 0].xyxy)
|
||||
|
||||
POSE_ESTIMATION_MODEL_ID = "usyd-community/vitpose-base-simple"
|
||||
|
||||
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
|
||||
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
|
||||
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = pose_estimation_model(**inputs)
|
||||
|
||||
results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes])
|
||||
key_point = sv.KeyPoints.from_transformers(results[0])
|
||||
```
|
||||
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
if "keypoints" in transfomers_results[0]:
|
||||
if transfomers_results[0]["keypoints"].cpu().numpy().size == 0:
|
||||
return cls.empty()
|
||||
|
||||
result_data = [
|
||||
(
|
||||
result["keypoints"].cpu().numpy(),
|
||||
result["scores"].cpu().numpy(),
|
||||
)
|
||||
for result in transfomers_results
|
||||
]
|
||||
|
||||
xy, scores = zip(*result_data)
|
||||
|
||||
return cls(
|
||||
xy=np.stack(xy).astype(np.float32),
|
||||
confidence=np.stack(scores).astype(np.float32),
|
||||
class_id=np.arange(len(xy)).astype(int),
|
||||
)
|
||||
else:
|
||||
return cls.empty()
|
||||
|
||||
def __getitem__(
|
||||
self, index: Union[int, slice, List[int], np.ndarray, str]
|
||||
) -> Union[KeyPoints, List, np.ndarray, None]:
|
||||
@@ -639,7 +726,7 @@ class KeyPoints:
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints.as_detections()
|
||||
|
||||
@@ -670,8 +670,7 @@ class MeanAverageRecallResult:
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_ylabel("Value", fontweight="bold")
|
||||
title = (
|
||||
f"Mean Average Recall, by Object Size"
|
||||
f"\n(target: {self.metric_target.value})"
|
||||
f"Mean Average Recall, by Object Size\n(target: {self.metric_target.value})"
|
||||
)
|
||||
ax.set_title(title, fontweight="bold")
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ def ensure_pandas_installed():
|
||||
raise ImportError(
|
||||
"`metrics` extra is required to run the function."
|
||||
" Run `pip install 'supervision[metrics]'` or"
|
||||
" `poetry add supervision -E metrics`"
|
||||
" `poetry add supervision -E metrics` or"
|
||||
" `uv pip install 'supervision[metrics]'`"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Callable, Generator, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -195,6 +196,9 @@ def process_video(
|
||||
source_path: str,
|
||||
target_path: str,
|
||||
callback: Callable[[np.ndarray, int], np.ndarray],
|
||||
max_frames: Optional[int] = None,
|
||||
show_progress: bool = False,
|
||||
progress_message: str = "Processing video",
|
||||
) -> None:
|
||||
"""
|
||||
Process a video file by applying a callback function on each frame
|
||||
@@ -207,6 +211,9 @@ def process_video(
|
||||
a numpy ndarray representation of a video frame and an
|
||||
int index of the frame and returns a processed numpy ndarray
|
||||
representation of the frame.
|
||||
max_frames (Optional[int]): The maximum number of frames to process.
|
||||
show_progress (bool): Whether to show a progress bar.
|
||||
progress_message (str): The message to display in the progress bar.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@@ -223,12 +230,31 @@ def process_video(
|
||||
```
|
||||
"""
|
||||
source_video_info = VideoInfo.from_video_path(video_path=source_path)
|
||||
video_frames_generator = get_video_frames_generator(
|
||||
source_path=source_path, end=max_frames
|
||||
)
|
||||
with VideoSink(target_path=target_path, video_info=source_video_info) as sink:
|
||||
if show_progress:
|
||||
# Calculate total_frames only when needed for tqdm
|
||||
total_frames = (
|
||||
min(source_video_info.total_frames, max_frames)
|
||||
if max_frames is not None
|
||||
else source_video_info.total_frames
|
||||
)
|
||||
for index, frame in enumerate(
|
||||
get_video_frames_generator(source_path=source_path)
|
||||
tqdm(
|
||||
video_frames_generator,
|
||||
total=total_frames,
|
||||
disable=not show_progress,
|
||||
desc=progress_message,
|
||||
)
|
||||
):
|
||||
result_frame = callback(frame, index)
|
||||
sink.write_frame(frame=result_frame)
|
||||
else:
|
||||
for index, frame in enumerate(video_frames_generator):
|
||||
result_frame = callback(frame, index)
|
||||
sink.write_frame(frame=result_frame)
|
||||
|
||||
|
||||
class FPSMonitor:
|
||||
|
||||
@@ -54,7 +54,7 @@ def validate_confidence(confidence: Any, n: int) -> None:
|
||||
|
||||
|
||||
def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
|
||||
expected_shape = f"({n,m})"
|
||||
expected_shape = f"({n, m})"
|
||||
actual_shape = str(getattr(confidence, "shape", None))
|
||||
|
||||
if confidence is not None:
|
||||
|
||||
@@ -408,8 +408,8 @@ def assert_csv_equal(file_name, expected_rows):
|
||||
with open(file_name, mode="r", newline="") as file:
|
||||
reader = csv.reader(file)
|
||||
for i, row in enumerate(reader):
|
||||
assert (
|
||||
[str(item) for item in expected_rows[i]] == row
|
||||
), f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
|
||||
assert [str(item) for item in expected_rows[i]] == row, (
|
||||
f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
|
||||
)
|
||||
|
||||
os.remove(file_name)
|
||||
|
||||
@@ -242,8 +242,8 @@ def test_json_sink(
|
||||
def assert_json_equal(file_name, expected_rows):
|
||||
with open(file_name, "r") as file:
|
||||
data = json.load(file)
|
||||
assert (
|
||||
data == expected_rows
|
||||
), f"Data in JSON file didn't match expected output: {data} != {expected_rows}"
|
||||
assert data == expected_rows, (
|
||||
f"Data in JSON file didn't match expected output: {data} != {expected_rows}"
|
||||
)
|
||||
|
||||
os.remove(file_name)
|
||||
|
||||
@@ -250,12 +250,12 @@ def test_line_zone_one_detection_default_anchors(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -415,12 +415,12 @@ def test_line_zone_one_detection(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -600,12 +600,12 @@ def test_line_zone_one_detection_long_horizon(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.lmm import from_paligemma
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"result, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
"",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty response
|
||||
(
|
||||
"",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty response with classes
|
||||
(
|
||||
"\n",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # new line response
|
||||
(
|
||||
"the quick brown fox jumps over the lazy dog.",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with no location
|
||||
(
|
||||
"<loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with missing location
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with extra location
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768>",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with no class
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> catt",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)),
|
||||
), # response with invalid class
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct response; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with space; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black-cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black-cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with hyphen; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black_cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black_cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with underscore; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ;",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768><loc0768> dog", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0, 1]),
|
||||
np.array(["cat", "dog"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768> cat", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partially correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768><loc0768><loc0768> cat", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partially correct response; with classes
|
||||
],
|
||||
)
|
||||
def test_from_paligemma(
|
||||
result: str,
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray],
|
||||
) -> None:
|
||||
result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes)
|
||||
np.testing.assert_array_equal(result[0], expected_results[0])
|
||||
np.testing.assert_array_equal(result[1], expected_results[1])
|
||||
np.testing.assert_array_equal(result[2], expected_results[2])
|
||||
@@ -21,6 +21,8 @@ from supervision.detection.utils import (
|
||||
scale_boxes,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
xyxy_to_xcycarh,
|
||||
xyxy_to_xywh,
|
||||
)
|
||||
|
||||
TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool)
|
||||
@@ -388,13 +390,13 @@ def test_process_roboflow_result(
|
||||
)
|
||||
for key in result[5]:
|
||||
if isinstance(result[5][key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[5][key], expected_result[5][key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[5][key], expected_result[5][key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[5][key] == expected_result[5][key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[5][key] == expected_result[5][key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1041,13 +1043,13 @@ def test_merge_data(
|
||||
|
||||
for key in result:
|
||||
if isinstance(result[key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[key], expected_result[key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[key], expected_result[key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[key] == expected_result[key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[key] == expected_result[key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1214,13 +1216,13 @@ def test_get_data_item(
|
||||
result = get_data_item(data=data, index=index)
|
||||
for key in result:
|
||||
if isinstance(result[key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[key], expected_result[key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[key], expected_result[key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[key] == expected_result[key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[key] == expected_result[key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1381,6 +1383,80 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, expected_result",
|
||||
[
|
||||
(np.array([[10, 20, 40, 60]]), np.array([[10, 20, 30, 40]])), # standard case
|
||||
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
|
||||
(
|
||||
np.array([[50, 50, 150, 150]]),
|
||||
np.array([[50, 50, 100, 100]]),
|
||||
), # large bounding box
|
||||
(
|
||||
np.array([[-10, -20, 20, 20]]),
|
||||
np.array([[-10, -20, 30, 40]]),
|
||||
), # negative coordinates
|
||||
(np.array([[50, 50, 50, 80]]), np.array([[50, 50, 0, 30]])), # zero width
|
||||
(np.array([[50, 50, 70, 50]]), np.array([[50, 50, 20, 0]])), # zero height
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
],
|
||||
)
|
||||
def test_xyxy_to_xywh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xyxy_to_xywh(xyxy)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, expected_result",
|
||||
[
|
||||
# Empty and zero cases
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
(
|
||||
np.array([[0, 0, 0, 0]]),
|
||||
np.array([[0, 0, 0.0, 0]]),
|
||||
), # zero size bounding box
|
||||
(
|
||||
np.array([[10, 10, 10, 10]]),
|
||||
np.array([[10, 10, 0.0, 0]]),
|
||||
), # point (x1=x2, y1=y2)
|
||||
# Zero width/height cases
|
||||
(np.array([[50, 50, 80, 50]]), np.array([[65, 50, 0.0, 0]])), # zero height
|
||||
(np.array([[50, 50, 50, 80]]), np.array([[50, 65, 0.0, 30]])), # zero width
|
||||
# Standard cases
|
||||
(np.array([[10, 20, 40, 60]]), np.array([[25, 40, 0.75, 40]])), # standard case
|
||||
(
|
||||
np.array([[-30, -40, -10, -20]]),
|
||||
np.array([[-20, -30, 1.0, 20]]),
|
||||
), # all negative values
|
||||
(
|
||||
np.array([[0.1, 0.2, 0.4, 0.6]]),
|
||||
np.array([[0.25, 0.4, 0.75, 0.4]]),
|
||||
), # values between 0-1
|
||||
# Different aspect ratios
|
||||
(
|
||||
np.array([[10, 20, 50, 100]]),
|
||||
np.array([[30, 60, 0.5, 80]]),
|
||||
), # tall rectangle (height > width)
|
||||
(
|
||||
np.array([[20, 10, 100, 50]]),
|
||||
np.array([[60, 30, 2.0, 40]]),
|
||||
), # wide rectangle (width > height)
|
||||
(
|
||||
np.array([[50, 50, 150, 150]]),
|
||||
np.array([[100, 100, 1.0, 100]]),
|
||||
), # height == width
|
||||
# Multiple boxes in one array
|
||||
(
|
||||
np.array([[0, 0, 0, 0], [10, 20, 40, 60]]),
|
||||
np.array([[0, 0, 0.0, 0], [25, 40, 0.75, 40]]),
|
||||
), # one zero-sized box and one normal box
|
||||
],
|
||||
)
|
||||
def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xyxy_to_xcycarh(xyxy)
|
||||
np.testing.assert_allclose(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xcycwh, expected_result",
|
||||
[
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.vlm import from_paligemma, from_qwen_2_5_vl
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception, result, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
does_not_raise(),
|
||||
"",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty text
|
||||
(
|
||||
does_not_raise(),
|
||||
"",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty text, classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"\n",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # newline only
|
||||
(
|
||||
does_not_raise(),
|
||||
"the quick brown fox jumps over the lazy dog.",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # random text, no location
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # partial location
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # extra loc
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768>",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # no class
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> catt",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)),
|
||||
), # invalid class
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # single box, no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with space
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black-cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black-cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with hyphen
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black_cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black_cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with underscore
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ;",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct class filter
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768><loc0768> dog",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0, 1]),
|
||||
np.array(["cat", "dog"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # multiple correct boxes, classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768> cat",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partial valid boxes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partial valid again
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(0, 1000),
|
||||
None,
|
||||
None,
|
||||
), # zero width -> ValueError
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<loc0256><loc0256><loc0768><loc0768> dog",
|
||||
(1000, -200),
|
||||
None,
|
||||
None,
|
||||
), # negative height -> ValueError
|
||||
],
|
||||
)
|
||||
def test_from_paligemma(
|
||||
exception,
|
||||
result: str,
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray],
|
||||
) -> None:
|
||||
with exception:
|
||||
result = from_paligemma(
|
||||
result=result, resolution_wh=resolution_wh, classes=classes
|
||||
)
|
||||
np.testing.assert_array_equal(result[0], expected_results[0])
|
||||
np.testing.assert_array_equal(result[1], expected_results[1])
|
||||
np.testing.assert_array_equal(result[2], expected_results[2])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception, result, input_wh, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
does_not_raise(),
|
||||
"some random text without triple backticks",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # no snippet
|
||||
(
|
||||
does_not_raise(),
|
||||
"```json\nnot valid json\n```",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # invalid JSON
|
||||
(
|
||||
does_not_raise(),
|
||||
"```json\n[]\n```",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # empty list
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 10, 100, 100]},
|
||||
{"label": "missing box"},
|
||||
{"bbox_2d": [50, 60, 110, 120], "unused": "something"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # missing keys
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(
|
||||
np.array([[20.0, 22.5, 220.0, 135.0]]),
|
||||
None,
|
||||
np.array(["cat"], dtype=str),
|
||||
),
|
||||
), # single box no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [0, 0, 64, 64], "label": "dog"},
|
||||
{"bbox_2d": [100, 200, 300, 400], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(640, 640),
|
||||
None,
|
||||
(
|
||||
np.array([[0, 0, 64, 64], [100, 200, 300, 400]], dtype=float),
|
||||
None,
|
||||
np.array(["dog", "cat"], dtype=str),
|
||||
),
|
||||
), # multiple no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "bird"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0, dtype=int), np.empty(0, dtype=str)),
|
||||
), # class mismatch
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"},
|
||||
{"bbox_2d": [50, 100, 150, 200], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(640, 480),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[10.0, 15.0, 110.0, 90.0], [50.0, 75.0, 150.0, 150.0]]),
|
||||
np.array([0, 1], dtype=int),
|
||||
np.array(["cat", "dog"], dtype=str),
|
||||
),
|
||||
), # partial filtering
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [-10, 0, 700, 700], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(
|
||||
np.array([[-20.0, 0.0, 1400.0, 787.5]]),
|
||||
None,
|
||||
np.array(["dog"], dtype=str),
|
||||
),
|
||||
), # out-of-bounds box
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(0, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
None, # won't be compared because we expect an exception
|
||||
), # zero input width -> ValueError
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, -100),
|
||||
None,
|
||||
None,
|
||||
), # negative resolution height -> ValueError
|
||||
],
|
||||
)
|
||||
def test_from_qwen_2_5_vl(
|
||||
exception,
|
||||
result: str,
|
||||
input_wh: Tuple[int, int],
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results,
|
||||
) -> None:
|
||||
with exception:
|
||||
xyxy, class_id, class_name = from_qwen_2_5_vl(
|
||||
result=result,
|
||||
input_wh=input_wh,
|
||||
resolution_wh=resolution_wh,
|
||||
classes=classes,
|
||||
)
|
||||
if expected_results is not None:
|
||||
np.testing.assert_array_equal(xyxy, expected_results[0])
|
||||
np.testing.assert_array_equal(class_id, expected_results[1])
|
||||
np.testing.assert_array_equal(class_name, expected_results[2])
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, Tuple
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.lmm import from_florence_2
|
||||
from supervision.detection.vlm import from_florence_2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -180,6 +180,6 @@ def test_generate_offset(
|
||||
)
|
||||
|
||||
# Verify that the generated offsets match the expected offsets
|
||||
assert np.array_equal(
|
||||
offsets, expected_offsets
|
||||
), f"Expected {expected_offsets}, got {offsets}"
|
||||
assert np.array_equal(offsets, expected_offsets), (
|
||||
f"Expected {expected_offsets}, got {offsets}"
|
||||
)
|
||||
|
||||
@@ -22,15 +22,15 @@ def test_ensure_cv2_image_for_processing_when_pillow_image_submitted(
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
), f"Parameter a expected to be {param_a_value} in target function"
|
||||
assert (
|
||||
param_b == param_b_value
|
||||
), f"Parameter b expected to be {param_b_value} in target function"
|
||||
assert np.allclose(image, empty_cv2_image), (
|
||||
"Expected conversion to OpenCV image to happen"
|
||||
)
|
||||
assert param_a == param_a_value, (
|
||||
f"Parameter a expected to be {param_a_value} in target function"
|
||||
)
|
||||
assert param_b == param_b_value, (
|
||||
f"Parameter b expected to be {param_b_value} in target function"
|
||||
)
|
||||
return image
|
||||
|
||||
# when
|
||||
@@ -61,15 +61,15 @@ def test_ensure_cv2_image_for_processing_when_cv2_image_submitted(
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
), f"Parameter a expected to be {param_a_value} in target function"
|
||||
assert (
|
||||
param_b == param_b_value
|
||||
), f"Parameter b expected to be {param_b_value} in target function"
|
||||
assert np.allclose(image, empty_cv2_image), (
|
||||
"Expected conversion to OpenCV image to happen"
|
||||
)
|
||||
assert param_a == param_a_value, (
|
||||
f"Parameter a expected to be {param_a_value} in target function"
|
||||
)
|
||||
assert param_b == param_b_value, (
|
||||
f"Parameter b expected to be {param_b_value} in target function"
|
||||
)
|
||||
return image
|
||||
|
||||
# when
|
||||
@@ -91,9 +91,9 @@ def test_cv2_to_pillow(
|
||||
|
||||
# then
|
||||
difference = ImageChops.difference(result, empty_pillow_image)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Conversion to PIL.Image expected not to change the content of image"
|
||||
assert difference.getbbox() is None, (
|
||||
"Conversion to PIL.Image expected not to change the content of image"
|
||||
)
|
||||
|
||||
|
||||
def test_pillow_to_cv2(
|
||||
@@ -103,9 +103,9 @@ def test_pillow_to_cv2(
|
||||
result = pillow_to_cv2(image=empty_pillow_image)
|
||||
|
||||
# then
|
||||
assert np.allclose(
|
||||
result, empty_cv2_image
|
||||
), "Conversion to OpenCV image expected not to change the content of image"
|
||||
assert np.allclose(result, empty_cv2_image), (
|
||||
"Conversion to OpenCV image expected not to change the content of image"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_empty_input_provided() -> None:
|
||||
@@ -128,9 +128,9 @@ def test_images_to_cv2_when_only_cv2_images_provided(
|
||||
# then
|
||||
assert len(result) == 5, "Expected the same number of output element as input ones"
|
||||
for result_element in result:
|
||||
assert (
|
||||
result_element is empty_cv2_image
|
||||
), "Expected CV images not to be touched by conversion"
|
||||
assert result_element is empty_cv2_image, (
|
||||
"Expected CV images not to be touched by conversion"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_only_pillow_images_provided(
|
||||
@@ -146,9 +146,9 @@ def test_images_to_cv2_when_only_pillow_images_provided(
|
||||
# then
|
||||
assert len(result) == 5, "Expected the same number of output element as input ones"
|
||||
for result_element in result:
|
||||
assert np.allclose(
|
||||
result_element, empty_cv2_image
|
||||
), "Output images expected to be equal to empty OpenCV image"
|
||||
assert np.allclose(result_element, empty_cv2_image), (
|
||||
"Output images expected to be equal to empty OpenCV image"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_mixed_input_provided(
|
||||
@@ -163,9 +163,9 @@ def test_images_to_cv2_when_mixed_input_provided(
|
||||
|
||||
# then
|
||||
assert len(result) == 2, "Expected the same number of output element as input ones"
|
||||
assert np.allclose(
|
||||
result[0], empty_cv2_image
|
||||
), "PIL image should be converted to OpenCV one, equal to example empty image"
|
||||
assert (
|
||||
result[1] is empty_cv2_image
|
||||
), "Expected CV images not to be touched by conversion"
|
||||
assert np.allclose(result[0], empty_cv2_image), (
|
||||
"PIL image should be converted to OpenCV one, equal to example empty image"
|
||||
)
|
||||
assert result[1] is empty_cv2_image, (
|
||||
"Expected CV images not to be touched by conversion"
|
||||
)
|
||||
|
||||
@@ -21,9 +21,9 @@ def test_resize_image_for_opencv_image() -> None:
|
||||
)
|
||||
|
||||
# then
|
||||
assert np.allclose(
|
||||
result, expected_result
|
||||
), "Expected output shape to be (w, h): (1024, 768)"
|
||||
assert np.allclose(result, expected_result), (
|
||||
"Expected output shape to be (w, h): (1024, 768)"
|
||||
)
|
||||
|
||||
|
||||
def test_resize_image_for_pillow_image() -> None:
|
||||
@@ -41,9 +41,9 @@ def test_resize_image_for_pillow_image() -> None:
|
||||
# then
|
||||
assert result.size == (1024, 768), "Expected output shape to be (w, h): (1024, 768)"
|
||||
difference = ImageChops.difference(result, expected_result)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Expected no difference in resized image content as the image is all zeros"
|
||||
assert difference.getbbox() is None, (
|
||||
"Expected no difference in resized image content as the image is all zeros"
|
||||
)
|
||||
|
||||
|
||||
def test_letterbox_image_for_opencv_image() -> None:
|
||||
@@ -95,9 +95,9 @@ def test_letterbox_image_for_pillow_image() -> None:
|
||||
1024,
|
||||
), "Expected output shape to be (w, h): (1024, 1024)"
|
||||
difference = ImageChops.difference(result, expected_result)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Expected padding to be added top and bottom with padding added top and bottom"
|
||||
assert difference.getbbox() is None, (
|
||||
"Expected padding to be added top and bottom with padding added top and bottom"
|
||||
)
|
||||
|
||||
|
||||
def test_create_tiles_with_one_image(
|
||||
|
||||
Reference in New Issue
Block a user