Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Publish to PyPI

on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
test:
name: Test on Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v7
- name: Install and test
run: |
uv venv --python ${{ matrix.python-version }}
uv pip install --group dev
uv run --no-project pytest

build:
name: Build and validate distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Build wheel and source distribution
run: |
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
- name: Verify version matches tag
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
python -c "import os, pathlib, sys; from packaging.utils import parse_sdist_filename, parse_wheel_filename; from packaging.version import Version; expected=Version(os.environ['RELEASE_TAG'].removeprefix('v')); files=list(pathlib.Path('dist').iterdir()); versions=[parse_wheel_filename(p.name)[1] if p.suffix == '.whl' else parse_sdist_filename(p.name)[1] for p in files]; sys.exit(0 if files and all(v == expected for v in versions) else f'artifact versions {versions} do not match tag {expected}')"
- name: Smoke-test the wheel
run: |
python -m venv /tmp/motmetrics-wheel-test
/tmp/motmetrics-wheel-test/bin/python -m pip install dist/*.whl
/tmp/motmetrics-wheel-test/bin/python -c "import motmetrics; print(motmetrics.__version__)"
- uses: actions/upload-artifact@v5
with:
name: python-package-distributions
path: dist/
if-no-files-found: error

publish:
name: Publish distribution to PyPI
needs: [test, build]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/motmetrics
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v6
with:
name: python-package-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
35 changes: 19 additions & 16 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,45 @@ name: Python package

on:
push:
branches: [develop]
branches: [master, develop]
tags: ["v*"]
pull_request:
branches: [develop]
branches: [master, develop]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: |
# The following is commented due to issue in package lap
# python -m pip install --upgrade pip
pip install --upgrade pip==22.0.3
python -m pip install flake8 pytest pytest-benchmark
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
uv venv --python ${{ matrix.python-version }}
uv pip install --group dev
# lapsolver is unmaintained and no longer builds with the C++
# toolchain on current GitHub-hosted runners. The test suite
# discovers optional solvers dynamically, so exercise the
# maintained LAP backends here instead.
pip install lap scipy "ortools<9.4" munkres
- name: Lint with flake8
uv pip install lap scipy munkres
uv pip install "ortools<9.12; python_version < '3.9'" "ortools; python_version >= '3.9'"
- name: Lint with Ruff
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
uv run --no-project ruff check motmetrics --select E9,F63,F7,F82
# report all configured lint findings without failing the build
uv run --no-project ruff check motmetrics --exit-zero
- name: Test with pytest
run: |
pytest
uv run --no-project pytest
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ COPY ./data /motmetrics/data
#RUN pip install motmetrics
RUN pip install -e ./motmetrics/py-motmetrics/

#RUN pip install -r motmetrics/py-motmetrics/requirements.txt

ENV GT_DIR motmetrics/data/train/
ENV TEST_DIR motmetrics/data/test/

#ENTRYPOINT python3 -m motmetrics.apps.eval_motchallenge motmetrics/data/train/ motmetrics/data/test/ && /bin/bash
CMD ["sh", "-c", "python3 -m motmetrics.apps.eval_motchallenge ${GT_DIR} ${TEST_DIR} && /bin/bash"]

3 changes: 0 additions & 3 deletions MANIFEST.in

This file was deleted.

98 changes: 47 additions & 51 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,37 +134,21 @@ To install **py-motmetrics** use `pip`
pip install motmetrics
```

Python 3.5/3.6/3.9 and numpy, pandas and scipy is required. If no binary packages are available for your platform and building source packages fails, you might want to try a distribution like Conda (see below) to install dependencies.
Python 3.8 through 3.14 and NumPy, pandas, and SciPy are required. If no binary packages are available for your platform and building source packages fails, you might want to try a distribution like Conda (see below) to install dependencies.

Alternatively for developing, clone or fork this repository and install in editing mode.

```
pip install -e <path/to/setup.py>
uv venv
uv pip install --group dev
```

### Install via Conda

In case you are using Conda, a simple way to run **py-motmetrics** is to create a virtual environment with all the necessary dependencies

```
conda env create -f environment.yml
> activate motmetrics-env
```

Then activate / source the `motmetrics-env` and install **py-motmetrics** and run the tests.

```
activate motmetrics-env
pip install .
pytest
```

In case you already have an environment you install the dependencies from within your environment by
To install the development dependencies and run the tests:

```
conda install --file requirements.txt
pip install .
pytest
uv venv
uv pip install --group dev
uv run --no-project pytest
```

## Usage
Expand Down Expand Up @@ -365,13 +349,16 @@ OVERALL 80.0% 80.0% 80.0% 80.0% 80.0% 4 2 2 0 2 2 1 1 50.0% 0.275
"""
```

#### [Underdeveloped] Computing HOTA metrics
#### Computing HOTA metrics

Computing HOTA metrics is also possible. However, it cannot be used with the `Accumulator` class directly, as HOTA requires to computing a reweighting matrix from all the frames at the beginning. Here is an example of how to use it:
HOTA cannot use `MOTAccumulator` directly because it requires a reweighting matrix computed from all frames. The example below computes HOTA over the standard alpha thresholds for one or more MOTChallenge sequences:

```python
import os

import numpy as np
import pandas as pd

import motmetrics as mm


Expand All @@ -384,35 +371,44 @@ def compute_motchallenge(dir_name):
res_list = mm.utils.compare_to_groundtruth_reweighting(df_gt, df_test, "iou", distth=th_list)
return res_list

# `data_dir` is the directory containing the gt.txt and test.txt files
acc = compute_motchallenge("data_dir")
mh = mm.metrics.create()

summary = mh.compute_many(
acc,
metrics=[
"deta_alpha",
"assa_alpha",
"hota_alpha",
],
generate_overall=True, # `Overall` is the average we need only
)
strsummary = mm.io.render_summary(
summary.iloc[[-1], :], # Use list to preserve `DataFrame` type
def compute_hota(sequence_dirs):
sequence_accs = [compute_motchallenge(path) for path in sequence_dirs]
mh = mm.metrics.create()
metrics = ["deta_alpha", "assa_alpha", "hota_alpha"]
alpha_results = []

for alpha_idx in range(len(sequence_accs[0])):
summary = mh.compute_many(
[accs[alpha_idx] for accs in sequence_accs],
metrics=metrics,
names=sequence_dirs,
generate_overall=True,
)
alpha_results.append(summary.loc["OVERALL"])

# HOTA, DetA, and AssA are averaged over the alpha thresholds.
result = pd.DataFrame(alpha_results).mean().to_frame().T
result.index = ["OVERALL"]
return result, mh


summary, mh = compute_hota([
"motmetrics/data/TUD-Campus",
"motmetrics/data/TUD-Stadtmitte",
])
print(mm.io.render_summary(
summary,
formatters=mh.formatters,
namemap={"hota_alpha": "HOTA", "assa_alpha": "ASSA", "deta_alpha": "DETA"},
)
print(strsummary)
"""
# data_dir=motmetrics/data/TUD-Campus
DETA ASSA HOTA
OVERALL 41.8% 36.9% 39.1%
# data_dir=motmetrics/data/TUD-Stadtmitte
DETA ASSA HOTA
OVERALL 39.2% 40.9% 39.8%
"""
namemap={"hota_alpha": "HOTA", "assa_alpha": "AssA", "deta_alpha": "DetA"},
))

# DetA AssA HOTA
# OVERALL 39.8% 41.2% 40.0%
```

When multiple sequences are supplied, `OVERALL` follows TrackEval's combination rules: detection counts are summed for DetA, AssA is weighted by detections, and HOTA is recomputed as `sqrt(DetA * AssA)` at each alpha before the final threshold average. This avoids incorrectly averaging per-sequence HOTA values.

### Computing distances

Up until this point we assumed the pairwise object/hypothesis distances to be known. Usually this is not the case. You are mostly given either rectangles or points (centroids) of related objects. To compute a distance matrix from them you can use `motmetrics.distance` module as shown below.
Expand Down Expand Up @@ -472,7 +468,7 @@ For large datasets solving the minimum cost assignment becomes the dominant runt
- `lapsolver` - https://github.com/cheind/py-lapsolver
- `lapjv` - https://github.com/gatagat/lap
- `scipy` - https://github.com/scipy/scipy/tree/master/scipy
- `ortools<9.4` - https://github.com/google/or-tools
- `ortools` - https://github.com/google/or-tools
- `munkres` - http://software.clapper.org/munkres/

A comparison for different sized matrices is shown below (taken from [here](https://github.com/cheind/py-lapsolver#benchmarks))
Expand Down
78 changes: 59 additions & 19 deletions Release.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,60 @@
# Release procedure

## Release

- git flow release start <VERSION>
- version bump motmetrics/__init__.py
- conda env create -f environment.yml
- activate motmetrics-env
- [pip install lapsolver]
- pip install .
- pytest
- deactivate
- conda env remove -n motmetrics-env
- git add, commit
- git flow release finish <VERSION>
- git push
- git push --tags
- git checkout master
- git push
- git checkout develop
- check appveyor, travis and pypi
Releases are built from Git tags and published by GitHub Actions using PyPI
Trusted Publishing. Maintainers do not need to store a long-lived PyPI token in
GitHub.

## One-time repository setup

1. In the PyPI settings for `motmetrics`, add a GitHub Trusted Publisher with:
- owner: `cheind`
- repository: `py-motmetrics`
- workflow: `publish-to-pypi.yml`
- environment: `pypi`
2. Create a protected GitHub environment named `pypi` and require maintainer
approval before deployment.
3. Protect `master` and require the Python package workflow to pass before a
release pull request can merge.

## Prepare a release

1. Create a short `release/<version>` branch from an up-to-date `master`.
2. Update `motmetrics.__version__` in `motmetrics/__init__.py` using a valid PEP
440 version without a leading `v`.
3. Update the changelog or release notes with user-visible changes.
4. Run the supported-version CI matrix and local checks:

python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
uv venv
uv pip install --group dev
uv run --no-project pytest

5. Install the generated wheel in a clean environment and smoke-test the
import and version.
6. Open a pull request to `master`, obtain review, and merge only after all
required checks pass.

## Publish

1. From the merged `master` commit, create and push a signed tag whose name is
the package version prefixed with `v`:

git tag -s v1.5.0 -m "Release 1.5.0"
git push origin v1.5.0

2. The `Publish to PyPI` workflow builds one source distribution and one
universal wheel with a current Python and setuptools 77 or newer, checks
their metadata, verifies the artifact version
against the tag, smoke-tests the wheel, and waits for approval on the `pypi`
environment before publishing.
3. After the workflow succeeds, create GitHub release notes for the same tag
and verify the published package:

python -m pip install --no-cache-dir motmetrics==1.5.0
python -c "import motmetrics; print(motmetrics.__version__)"

PyPI files are immutable. If publishing fails after any artifact was accepted,
increment the version and publish a new release; never reuse or move a release
tag.
10 changes: 0 additions & 10 deletions environment.yml

This file was deleted.

Loading