Spaces:
Runtime error
Runtime error
Init Upload
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +79 -0
- .gitignore +183 -0
- Dockerfile +131 -0
- models/__init__.py +0 -0
- models/base/__init__.py +2 -0
- models/base/onnx_base.py +58 -0
- models/base/trt_base.py +97 -0
- models/detectors/__init__.py +0 -0
- models/detectors/yolov7.py +228 -0
- models/models/__init__.py +0 -0
- models/models/base/__init__.py +0 -0
- models/models/base/onnx_base.py +68 -0
- models/models/base/trt_base.py +96 -0
- models/models/detectors/__init__.py +0 -0
- models/models/detectors/mmyolov8.py +86 -0
- models/models/detectors/yolov7.py +251 -0
- models/models/engine/__init__.py +0 -0
- models/models/engine/threading_func.py +221 -0
- models/models/engine/utils.py +248 -0
- models/models/engine/visualizer.py +143 -0
- models/models/pose/rtmpose.py +193 -0
- models/models/reids/__init__.py +0 -0
- models/models/reids/solider.py +165 -0
- models/models/trackers/__init__.py +2 -0
- models/models/trackers/byte_track.py +74 -0
- models/models/trackers/reid_parallel_tracker/__init__.py +1 -0
- models/models/trackers/reid_parallel_tracker/base_tracker.py +315 -0
- models/models/trackers/reid_parallel_tracker/core/__init__.py +0 -0
- models/models/trackers/reid_parallel_tracker/core/basetrack.py +165 -0
- models/models/trackers/reid_parallel_tracker/core/homography.py +89 -0
- models/models/trackers/reid_parallel_tracker/core/kalman_filter.py +275 -0
- models/models/trackers/reid_parallel_tracker/core/matching.py +405 -0
- models/models/trackers/reid_parallel_tracker/core/tracklet.py +427 -0
- models/models/trackers/reid_parallel_tracker/matchers/__init__.py +4 -0
- models/models/trackers/reid_parallel_tracker/matchers/base_matchers.py +39 -0
- models/models/trackers/reid_parallel_tracker/matchers/distances.py +132 -0
- models/models/trackers/reid_parallel_tracker/matchers/prioritize_reid_matcher.py +224 -0
- models/models/trackers/reid_parallel_tracker/matchers/single_stage_matcher.py +39 -0
- models/models/trackers/reid_parallel_tracker/parallel_tracker.py +470 -0
- models/models/trackers/reid_parallel_tracker/three_stage_tracker.py +132 -0
- models/reids/__init__.py +0 -0
- models/reids/solider.py +87 -0
- models/trackers/__init__.py +0 -0
- models/trackers/byte_track.py +56 -0
- projects/human_detection/ReadMe.md +43 -0
- projects/human_detection/demo_app.py +147 -0
- projects/human_detection/docker_run.sh +11 -0
- projects/human_detection/engine/pipeline.py +81 -0
- projects/human_detection/engine/threading_func.py +43 -0
- projects/human_detection/engine/visualizer.py +51 -0
.dockerignore
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
*.Dockerfile
|
| 3 |
+
.DS_Store
|
| 4 |
+
.gitignore
|
| 5 |
+
.dockerignore
|
| 6 |
+
|
| 7 |
+
/credentials
|
| 8 |
+
/cache
|
| 9 |
+
/store
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
|
| 13 |
+
|
| 14 |
+
# Byte-compiled / optimized / DLL files
|
| 15 |
+
__pycache__/
|
| 16 |
+
*.py[cod]
|
| 17 |
+
*$py.class
|
| 18 |
+
|
| 19 |
+
# C extensions
|
| 20 |
+
*.so
|
| 21 |
+
|
| 22 |
+
# Distribution / packaging
|
| 23 |
+
.Python
|
| 24 |
+
build/
|
| 25 |
+
develop-eggs/
|
| 26 |
+
dist/
|
| 27 |
+
downloads/
|
| 28 |
+
eggs/
|
| 29 |
+
.eggs/
|
| 30 |
+
lib64/
|
| 31 |
+
parts/
|
| 32 |
+
sdist/
|
| 33 |
+
var/
|
| 34 |
+
wheels/
|
| 35 |
+
*.egg-info/
|
| 36 |
+
.installed.cfg
|
| 37 |
+
*.egg
|
| 38 |
+
|
| 39 |
+
# PyInstaller
|
| 40 |
+
# Usually these files are written by a python script from a template
|
| 41 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 42 |
+
*.manifest
|
| 43 |
+
*.spec
|
| 44 |
+
|
| 45 |
+
# Installer logs
|
| 46 |
+
pip-log.txt
|
| 47 |
+
pip-delete-this-directory.txt
|
| 48 |
+
|
| 49 |
+
# Flask stuff:
|
| 50 |
+
instance/
|
| 51 |
+
.webassets-cache
|
| 52 |
+
|
| 53 |
+
# Scrapy stuff:
|
| 54 |
+
.scrapy
|
| 55 |
+
|
| 56 |
+
# Sphinx documentation
|
| 57 |
+
docs/_build/
|
| 58 |
+
|
| 59 |
+
# PyBuilder
|
| 60 |
+
target/
|
| 61 |
+
|
| 62 |
+
# Jupyter Notebook
|
| 63 |
+
.ipynb_checkpoints
|
| 64 |
+
|
| 65 |
+
# pyenv
|
| 66 |
+
.python-version
|
| 67 |
+
|
| 68 |
+
# Environments
|
| 69 |
+
.env
|
| 70 |
+
.venv
|
| 71 |
+
env/
|
| 72 |
+
venv/
|
| 73 |
+
ENV/
|
| 74 |
+
|
| 75 |
+
# ignore all markdown files (md) beside all README*.md other than README-secret.md
|
| 76 |
+
*.md
|
| 77 |
+
*.egg-info/
|
| 78 |
+
thunder-collection*.json
|
| 79 |
+
*.code-workspace
|
.gitignore
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
traffic_monitoring/detection/vis_img/
|
| 2 |
+
traffic_monitoring/detection/vis_video/
|
| 3 |
+
web/output/
|
| 4 |
+
|
| 5 |
+
# Byte-compiled / optimized / DLL files
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
|
| 10 |
+
# C extensions
|
| 11 |
+
*.so
|
| 12 |
+
|
| 13 |
+
# Distribution / packaging
|
| 14 |
+
.Python
|
| 15 |
+
build/
|
| 16 |
+
develop-eggs/
|
| 17 |
+
dist/
|
| 18 |
+
downloads/
|
| 19 |
+
eggs/
|
| 20 |
+
.eggs/
|
| 21 |
+
lib/
|
| 22 |
+
lib64/
|
| 23 |
+
parts/
|
| 24 |
+
sdist/
|
| 25 |
+
var/
|
| 26 |
+
wheels/
|
| 27 |
+
share/python-wheels/
|
| 28 |
+
*.egg-info/
|
| 29 |
+
.installed.cfg
|
| 30 |
+
*.egg
|
| 31 |
+
MANIFEST
|
| 32 |
+
|
| 33 |
+
# PyInstaller
|
| 34 |
+
# Usually these files are written by a python script from a template
|
| 35 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 36 |
+
*.manifest
|
| 37 |
+
*.spec
|
| 38 |
+
|
| 39 |
+
# Installer logs
|
| 40 |
+
pip-log.txt
|
| 41 |
+
pip-delete-this-directory.txt
|
| 42 |
+
|
| 43 |
+
# Unit test / coverage reports
|
| 44 |
+
htmlcov/
|
| 45 |
+
.tox/
|
| 46 |
+
.nox/
|
| 47 |
+
.coverage
|
| 48 |
+
.coverage.*
|
| 49 |
+
.cache
|
| 50 |
+
nosetests.xml
|
| 51 |
+
coverage.xml
|
| 52 |
+
*.cover
|
| 53 |
+
*.py,cover
|
| 54 |
+
.hypothesis/
|
| 55 |
+
.pytest_cache/
|
| 56 |
+
cover/
|
| 57 |
+
|
| 58 |
+
# Translations
|
| 59 |
+
*.mo
|
| 60 |
+
*.pot
|
| 61 |
+
|
| 62 |
+
# Django stuff:
|
| 63 |
+
*.log
|
| 64 |
+
local_settings.py
|
| 65 |
+
db.sqlite3
|
| 66 |
+
db.sqlite3-journal
|
| 67 |
+
|
| 68 |
+
# Flask stuff:
|
| 69 |
+
instance/
|
| 70 |
+
.webassets-cache
|
| 71 |
+
|
| 72 |
+
# Scrapy stuff:
|
| 73 |
+
.scrapy
|
| 74 |
+
|
| 75 |
+
# Sphinx documentation
|
| 76 |
+
docs/_build/
|
| 77 |
+
|
| 78 |
+
# PyBuilder
|
| 79 |
+
.pybuilder/
|
| 80 |
+
target/
|
| 81 |
+
|
| 82 |
+
# Jupyter Notebook
|
| 83 |
+
.ipynb_checkpoints
|
| 84 |
+
|
| 85 |
+
# IPython
|
| 86 |
+
profile_default/
|
| 87 |
+
ipython_config.py
|
| 88 |
+
|
| 89 |
+
# pyenv
|
| 90 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 91 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 92 |
+
# .python-version
|
| 93 |
+
|
| 94 |
+
# pipenv
|
| 95 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 96 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 97 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 98 |
+
# install all needed dependencies.
|
| 99 |
+
#Pipfile.lock
|
| 100 |
+
|
| 101 |
+
# poetry
|
| 102 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 103 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 104 |
+
# commonly ignored for libraries.
|
| 105 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 106 |
+
#poetry.lock
|
| 107 |
+
|
| 108 |
+
# pdm
|
| 109 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 110 |
+
#pdm.lock
|
| 111 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 112 |
+
# in version control.
|
| 113 |
+
# https://pdm.fming.dev/#use-with-ide
|
| 114 |
+
.pdm.toml
|
| 115 |
+
|
| 116 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 117 |
+
__pypackages__/
|
| 118 |
+
|
| 119 |
+
# Celery stuff
|
| 120 |
+
celerybeat-schedule
|
| 121 |
+
celerybeat.pid
|
| 122 |
+
|
| 123 |
+
# SageMath parsed files
|
| 124 |
+
*.sage.py
|
| 125 |
+
|
| 126 |
+
# Environments
|
| 127 |
+
.env
|
| 128 |
+
.venv
|
| 129 |
+
env/
|
| 130 |
+
venv/
|
| 131 |
+
ENV/
|
| 132 |
+
env.bak/
|
| 133 |
+
venv.bak/
|
| 134 |
+
|
| 135 |
+
# Spyder project settings
|
| 136 |
+
.spyderproject
|
| 137 |
+
.spyproject
|
| 138 |
+
|
| 139 |
+
# Rope project settings
|
| 140 |
+
.ropeproject
|
| 141 |
+
|
| 142 |
+
# mkdocs documentation
|
| 143 |
+
/site
|
| 144 |
+
|
| 145 |
+
# mypy
|
| 146 |
+
.mypy_cache/
|
| 147 |
+
.dmypy.json
|
| 148 |
+
dmypy.json
|
| 149 |
+
|
| 150 |
+
# Pyre type checker
|
| 151 |
+
.pyre/
|
| 152 |
+
|
| 153 |
+
# pytype static type analyzer
|
| 154 |
+
.pytype/
|
| 155 |
+
|
| 156 |
+
# Cython debug symbols
|
| 157 |
+
cython_debug/
|
| 158 |
+
|
| 159 |
+
# PyCharm
|
| 160 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 161 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 162 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 163 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 164 |
+
#.idea/
|
| 165 |
+
*.db
|
| 166 |
+
*.avi
|
| 167 |
+
vis/
|
| 168 |
+
cache/
|
| 169 |
+
uploads/
|
| 170 |
+
web/static
|
| 171 |
+
.flaskenv
|
| 172 |
+
mprofile_*
|
| 173 |
+
migrations/
|
| 174 |
+
core.*
|
| 175 |
+
ckpts/
|
| 176 |
+
.devcontainer/
|
| 177 |
+
.vscode/
|
| 178 |
+
*.pth
|
| 179 |
+
*.zip
|
| 180 |
+
*.trt
|
| 181 |
+
*.engine
|
| 182 |
+
timing.cache
|
| 183 |
+
timing.cache.lock
|
Dockerfile
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### This Dockerfile is modified from MMDeploy to build MMDeploy for GPU devices
|
| 2 |
+
### We update the tensorrt version and cuda version to 11.8
|
| 3 |
+
FROM nvcr.io/nvidia/tensorrt:23.08-py3
|
| 4 |
+
|
| 5 |
+
ARG CUDA=11.8
|
| 6 |
+
ARG PYTHON_VERSION=3.10
|
| 7 |
+
ARG TORCH_VERSION=2.0.0
|
| 8 |
+
ARG TORCHVISION_VERSION=0.15.1
|
| 9 |
+
ARG ONNXRUNTIME_VERSION=1.15.1
|
| 10 |
+
ARG PPLCV_VERSION=0.7.0
|
| 11 |
+
ENV FORCE_CUDA="1"
|
| 12 |
+
ARG MMCV_VERSION="==2.0.0"
|
| 13 |
+
ARG MMENGINE_VERSION="==0.8.4"
|
| 14 |
+
|
| 15 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 16 |
+
|
| 17 |
+
### change the system source for installing libs
|
| 18 |
+
ARG USE_SRC_INSIDE=false
|
| 19 |
+
RUN if [ ${USE_SRC_INSIDE} == true ] ; \
|
| 20 |
+
then \
|
| 21 |
+
sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list ; \
|
| 22 |
+
sed -i s/security.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list ; \
|
| 23 |
+
echo "Use aliyun source for installing libs" ; \
|
| 24 |
+
else \
|
| 25 |
+
echo "Keep the download source unchanged" ; \
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
### update apt and install libs
|
| 29 |
+
RUN apt-get update &&\
|
| 30 |
+
apt-get install -y vim libsm6 libxext6 libxrender-dev libgl1-mesa-glx git wget libssl-dev libopencv-dev libspdlog-dev --no-install-recommends &&\
|
| 31 |
+
rm -rf /var/lib/apt/lists/*
|
| 32 |
+
|
| 33 |
+
RUN curl -fsSL -v -o ~/miniconda.sh -O https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
|
| 34 |
+
chmod +x ~/miniconda.sh && \
|
| 35 |
+
bash ~/miniconda.sh -b -p /opt/conda && \
|
| 36 |
+
rm ~/miniconda.sh && \
|
| 37 |
+
/opt/conda/bin/conda install -y python=${PYTHON_VERSION} conda-build pyyaml numpy ipython cython typing typing_extensions mkl mkl-include ninja && \
|
| 38 |
+
/opt/conda/bin/conda clean -ya
|
| 39 |
+
|
| 40 |
+
### change the pip source for installing packages
|
| 41 |
+
RUN if [ ${USE_SRC_INSIDE} == true ] ; \
|
| 42 |
+
then \
|
| 43 |
+
/opt/conda/bin/pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple; \
|
| 44 |
+
echo "pip using tsinghua source" ; \
|
| 45 |
+
else \
|
| 46 |
+
echo "Keep pip the download source unchanged" ; \
|
| 47 |
+
fi
|
| 48 |
+
|
| 49 |
+
### install pytorch openmim
|
| 50 |
+
RUN /opt/conda/bin/conda install pytorch==${TORCH_VERSION} torchvision==${TORCHVISION_VERSION} cudatoolkit=${CUDA} -c pytorch -c conda-forge -y \
|
| 51 |
+
&& /opt/conda/bin/pip install --no-cache-dir openmim
|
| 52 |
+
|
| 53 |
+
### pytorch mmcv onnxruntime
|
| 54 |
+
RUN /opt/conda/bin/mim install --no-cache-dir "mmcv"${MMCV_VERSION} onnxruntime-gpu==${ONNXRUNTIME_VERSION} mmengine${MMENGINE_VERSION}
|
| 55 |
+
|
| 56 |
+
ENV PATH /opt/conda/bin:$PATH
|
| 57 |
+
WORKDIR /root/workspace
|
| 58 |
+
|
| 59 |
+
### get onnxruntime
|
| 60 |
+
RUN wget https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz \
|
| 61 |
+
&& tar -zxvf onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz
|
| 62 |
+
|
| 63 |
+
### cp trt from pip to conda
|
| 64 |
+
RUN cp -r /usr/local/lib/python${PYTHON_VERSION}/dist-packages/tensorrt* /opt/conda/lib/python${PYTHON_VERSION}/site-packages/
|
| 65 |
+
|
| 66 |
+
### install mmdeploy
|
| 67 |
+
ENV ONNXRUNTIME_DIR=/root/workspace/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}
|
| 68 |
+
ENV TENSORRT_DIR=/workspace/tensorrt
|
| 69 |
+
ARG VERSION
|
| 70 |
+
RUN git clone -b main https://github.com/open-mmlab/mmdeploy &&\
|
| 71 |
+
cd mmdeploy &&\
|
| 72 |
+
if [ -z ${VERSION} ] ; then echo "No MMDeploy version passed in, building on main" ; else git checkout tags/v${VERSION} -b tag_v${VERSION} ; fi &&\
|
| 73 |
+
git submodule update --init --recursive &&\
|
| 74 |
+
mkdir -p build &&\
|
| 75 |
+
cd build &&\
|
| 76 |
+
cmake -DMMDEPLOY_TARGET_BACKENDS="ort;trt" .. &&\
|
| 77 |
+
make -j$(nproc) &&\
|
| 78 |
+
cd .. &&\
|
| 79 |
+
/opt/conda/bin/mim install -e .
|
| 80 |
+
|
| 81 |
+
### build sdk
|
| 82 |
+
# RUN git clone https://github.com/openppl-public/ppl.cv.git &&\
|
| 83 |
+
# cd ppl.cv &&\
|
| 84 |
+
# git checkout tags/v${PPLCV_VERSION} -b v${PPLCV_VERSION} &&\
|
| 85 |
+
# ./build.sh cuda
|
| 86 |
+
|
| 87 |
+
ENV BACKUP_LD_LIBRARY_PATH=$LD_LIBRARY_PATH
|
| 88 |
+
ENV LD_LIBRARY_PATH=/usr/local/cuda/compat/lib.real/:$LD_LIBRARY_PATH
|
| 89 |
+
|
| 90 |
+
RUN cd /root/workspace/mmdeploy &&\
|
| 91 |
+
rm -rf build/CM* build/cmake-install.cmake build/Makefile build/csrc &&\
|
| 92 |
+
mkdir -p build && cd build &&\
|
| 93 |
+
cmake .. \
|
| 94 |
+
-DMMDEPLOY_BUILD_EXAMPLES=ON \
|
| 95 |
+
-DCMAKE_CXX_COMPILER=g++ \
|
| 96 |
+
-DTENSORRT_DIR=${TENSORRT_DIR} \
|
| 97 |
+
-DONNXRUNTIME_DIR=${ONNXRUNTIME_DIR} \
|
| 98 |
+
-DMMDEPLOY_BUILD_SDK_PYTHON_API=ON \
|
| 99 |
+
-DMMDEPLOY_TARGET_DEVICES="cuda;cpu" \
|
| 100 |
+
-DMMDEPLOY_TARGET_BACKENDS="ort;trt" \
|
| 101 |
+
-DMMDEPLOY_CODEBASES=all &&\
|
| 102 |
+
make -j$(nproc) && make install &&\
|
| 103 |
+
export SPDLOG_LEVEL=warn &&\
|
| 104 |
+
if [ -z ${VERSION} ] ; then echo "Built MMDeploy for GPU devices successfully!" ; else echo "Built MMDeploy version v${VERSION} for GPU devices successfully!" ; fi
|
| 105 |
+
# -DMMDEPLOY_BUILD_SDK=ON \
|
| 106 |
+
# -Dpplcv_DIR=/root/workspace/ppl.cv/cuda-build/install/lib/cmake/ppl \
|
| 107 |
+
|
| 108 |
+
ENV LD_LIBRARY_PATH="/root/workspace/mmdeploy/build/lib:${BACKUP_LD_LIBRARY_PATH}"
|
| 109 |
+
ENV CUDA_HOME /usr/local/cuda-11.8/
|
| 110 |
+
WORKDIR /root/workspace/cc-demo
|
| 111 |
+
|
| 112 |
+
COPY . /root/workspace/cc-demo
|
| 113 |
+
RUN mkdir -p /data && mv ./data /data/human_detection
|
| 114 |
+
RUN rm -rf /var/lib/apt/lists/*
|
| 115 |
+
RUN apt-get update -y
|
| 116 |
+
RUN apt-get install ffmpeg libsm6 libxext6 -y
|
| 117 |
+
RUN apt-get clean -y
|
| 118 |
+
RUN pip install --upgrade pip
|
| 119 |
+
|
| 120 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 121 |
+
RUN mim install mmdet==3.1.0
|
| 122 |
+
RUN mim install mmpose==1.1.0
|
| 123 |
+
RUN mim install mmyolo==0.6.0
|
| 124 |
+
RUN pip install cython-bbox==0.1.3
|
| 125 |
+
RUN pip install lap
|
| 126 |
+
RUN pip install gradio
|
| 127 |
+
RUN python setup.py develop
|
| 128 |
+
|
| 129 |
+
RUN echo 'export PYTHONPATH=$PYTHONPATH:./' >> ~/.bashrc
|
| 130 |
+
|
| 131 |
+
CMD /bin/bash -c "cd projects && gradio human_detection/demo_app.py"
|
models/__init__.py
ADDED
|
File without changes
|
models/base/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .trt_base import TRT_Base
|
| 2 |
+
from .onnx_base import ONNX_Base
|
models/base/onnx_base.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference for onnx model (.onnx)
|
| 2 |
+
from typing import List
|
| 3 |
+
import numpy as np
|
| 4 |
+
import onnxruntime as ort
|
| 5 |
+
import os, torch
|
| 6 |
+
|
| 7 |
+
class ONNX_Base():
|
| 8 |
+
def __init__(self,
|
| 9 |
+
model_path: str,
|
| 10 |
+
device: str='0'):
|
| 11 |
+
self.model_path = model_path
|
| 12 |
+
self.device = self.select_device(device)
|
| 13 |
+
self.session = self.create_session(model_path)
|
| 14 |
+
|
| 15 |
+
def create_session(self, model_path):
|
| 16 |
+
|
| 17 |
+
providers = ['CPUExecutionProvider']
|
| 18 |
+
if torch.cuda.is_available():
|
| 19 |
+
providers.insert(0, 'CUDAExecutionProvider')
|
| 20 |
+
ort_session = ort.InferenceSession(model_path, providers=providers)
|
| 21 |
+
return ort_session
|
| 22 |
+
|
| 23 |
+
def select_device(self, device: str)->torch.device:
|
| 24 |
+
""" Select device to be used for inference.
|
| 25 |
+
Args:
|
| 26 |
+
param device: 'cpu' or '0' or '0,1,2,3'
|
| 27 |
+
Return:
|
| 28 |
+
torch.device
|
| 29 |
+
"""
|
| 30 |
+
cpu = device.lower() == "cpu"
|
| 31 |
+
if cpu:
|
| 32 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
| 33 |
+
return torch.device("cpu")
|
| 34 |
+
else:
|
| 35 |
+
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested'
|
| 36 |
+
os.environ['CUDA_VISBILE_DEVICES'] = device
|
| 37 |
+
torch.cuda.set_device(int(device))
|
| 38 |
+
return torch.device(f"cuda:{device}")
|
| 39 |
+
|
| 40 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[np.ndarray]:
|
| 41 |
+
""" Inference for onnx model.
|
| 42 |
+
Args:
|
| 43 |
+
param image_batch: (batch_size, height, width, channels)
|
| 44 |
+
Return:
|
| 45 |
+
results: List[np.ndarray]
|
| 46 |
+
"""
|
| 47 |
+
input_name = self.session.get_inputs()[0].name
|
| 48 |
+
results = self.session.run(None, {input_name: image_batch})
|
| 49 |
+
return results
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
|
models/base/trt_base.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from typing import Tuple, Dict,List
|
| 3 |
+
import tensorrt as trt
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import time
|
| 7 |
+
import os
|
| 8 |
+
from collections import OrderedDict, namedtuple
|
| 9 |
+
|
| 10 |
+
class TRT_Base():
|
| 11 |
+
def __init__(self,
|
| 12 |
+
input_shape: Tuple[int, int, int],
|
| 13 |
+
model_path: str,
|
| 14 |
+
device: str='0'):
|
| 15 |
+
""" Tensor RT base class for inference.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 19 |
+
model_path (str): path to the model.trt
|
| 20 |
+
device (str, optional): CUDA device. Defaults to '0'.
|
| 21 |
+
"""
|
| 22 |
+
self.input_shape = input_shape
|
| 23 |
+
self.model_path = model_path
|
| 24 |
+
self.device = self.select_device(device)
|
| 25 |
+
self.init_model()
|
| 26 |
+
|
| 27 |
+
def select_device(self, device: str)->torch.device:
|
| 28 |
+
""" Select device to be used for inference.
|
| 29 |
+
Args:
|
| 30 |
+
param device: 'cpu' or '0' or '0,1,2,3'
|
| 31 |
+
Return:
|
| 32 |
+
torch.device
|
| 33 |
+
"""
|
| 34 |
+
cpu = device.lower() == "cpu"
|
| 35 |
+
if cpu:
|
| 36 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
| 37 |
+
return torch.device("cpu")
|
| 38 |
+
else:
|
| 39 |
+
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested'
|
| 40 |
+
torch.cuda.set_device(int(device))
|
| 41 |
+
return torch.device("cuda")
|
| 42 |
+
|
| 43 |
+
def init_model(self):
|
| 44 |
+
""" Initialize TensorRT engine and context."""
|
| 45 |
+
logger = trt.Logger(trt.Logger.INFO)
|
| 46 |
+
trt.init_libnvinfer_plugins(logger, namespace="")
|
| 47 |
+
with open(self.model_path, 'rb') as f, trt.Runtime(logger) as runtime:
|
| 48 |
+
engine = runtime.deserialize_cuda_engine(f.read())
|
| 49 |
+
context = engine.create_execution_context()
|
| 50 |
+
self.model = {
|
| 51 |
+
"engine": engine,
|
| 52 |
+
"context": context
|
| 53 |
+
}
|
| 54 |
+
bindings, binding_addrs = self.get_bindings(input_shape=self.input_shape)
|
| 55 |
+
input_names = [binding_name for binding_name in binding_addrs.keys() if (self.model["engine"].binding_is_input(binding_name))]
|
| 56 |
+
|
| 57 |
+
for _ in range(10):
|
| 58 |
+
for name in input_names:
|
| 59 |
+
binding_addrs[name] = int(torch.randn(bindings[name].shape).to(self.device).data_ptr())
|
| 60 |
+
context.execute_v2(list(binding_addrs.values()))
|
| 61 |
+
self.model.update({
|
| 62 |
+
'binding_addrs': binding_addrs,
|
| 63 |
+
'bindings': bindings,
|
| 64 |
+
'rt_shapes': self.input_shape
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
def get_bindings(self, input_shape: Tuple[int, int, int]):
|
| 68 |
+
""" Get bindings and binding addresses for TensorRT engine.
|
| 69 |
+
Args:
|
| 70 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
self.model["context"].set_binding_shape(0, input_shape)
|
| 74 |
+
bindings = OrderedDict()
|
| 75 |
+
Binding = namedtuple('Binding', ('name', 'dtype', 'shape', 'data', 'ptr'))
|
| 76 |
+
for index in range(self.model["engine"].num_bindings):
|
| 77 |
+
name = self.model["engine"].get_binding_name(index)
|
| 78 |
+
dtype = trt.nptype(self.model["engine"].get_binding_dtype(index))
|
| 79 |
+
shape = tuple(self.model["context"].get_binding_shape(index))
|
| 80 |
+
data = torch.from_numpy(np.empty(shape, dtype=np.dtype(dtype))).to(self.device)
|
| 81 |
+
bindings[name] = Binding(name, dtype, shape, data, int(data.data_ptr()))
|
| 82 |
+
binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items())
|
| 83 |
+
return bindings, binding_addrs
|
| 84 |
+
|
| 85 |
+
def change_runtime_dimension(self, input_shape: Tuple[int, int, int]):
|
| 86 |
+
""" Support inference with Dynamic shape.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 90 |
+
"""
|
| 91 |
+
if (input_shape == self.model["rt_shapes"]): return
|
| 92 |
+
bindings, binding_addrs = self.get_bindings(input_shape)
|
| 93 |
+
self.model['binding_addrs'] = binding_addrs
|
| 94 |
+
self.model['bindings'] = bindings
|
| 95 |
+
self.model['rt_shapes'] = input_shape
|
| 96 |
+
|
| 97 |
+
|
models/detectors/__init__.py
ADDED
|
File without changes
|
models/detectors/yolov7.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Tuple, Optional, Union
|
| 2 |
+
import torch
|
| 3 |
+
import time
|
| 4 |
+
from models.base.trt_base import TRT_Base
|
| 5 |
+
from models.base.onnx_base import ONNX_Base
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
from mmcv.ops.nms import batched_nms
|
| 9 |
+
|
| 10 |
+
class YOLOv7Base():
|
| 11 |
+
def __init__(self,
|
| 12 |
+
class_map_ids: Optional[Dict]=None,
|
| 13 |
+
preprocess_cfg: Dict=dict(
|
| 14 |
+
border_color=(114, 114, 114),
|
| 15 |
+
auto=False,
|
| 16 |
+
scaleFill=True,
|
| 17 |
+
scaleup=True,
|
| 18 |
+
stride=32),
|
| 19 |
+
nms_agnostic_cfg: Dict=dict(
|
| 20 |
+
type='nms',
|
| 21 |
+
iou_threshold=0.9,
|
| 22 |
+
class_agnostic=True),
|
| 23 |
+
use_torch: bool=False,
|
| 24 |
+
):
|
| 25 |
+
""" YOLOv7 class for inference.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
class_map_ids (Dict): class mapping dictionary to map model's class to original class. Eg {0: 1, 1: 0, 2: 2, 3: 3} mean we swap class ID between 0 and 1.
|
| 29 |
+
preprocess_cfg (Dict):
|
| 30 |
+
- border_color (Tuple[int, int, int]): padding value.
|
| 31 |
+
- auto (bool): resize input to minimum rectangle.
|
| 32 |
+
- scaleFill (bool): stretching the input image.
|
| 33 |
+
- scaleUp (bool): allow scale up the input image.
|
| 34 |
+
- stride (int): stride of the model.
|
| 35 |
+
nms_agnostic_cfg (Dict):
|
| 36 |
+
- type (str): nms type (nms, softnms).
|
| 37 |
+
- iou_threshold (float): IoU threshold of NMS.
|
| 38 |
+
- class_agnostic (bool): enable class-agnostic NMS instead of NMS for each class.
|
| 39 |
+
use_torch (bool): use torch tensor or numpy array in preprocess and postprocess function.
|
| 40 |
+
"""
|
| 41 |
+
self.preprocess_cfg=preprocess_cfg
|
| 42 |
+
self.nms_agnostic_cfg=nms_agnostic_cfg
|
| 43 |
+
self.class_map_ids = class_map_ids
|
| 44 |
+
self.use_torch= use_torch
|
| 45 |
+
|
| 46 |
+
def letterbox(self,
|
| 47 |
+
img: np.ndarray,
|
| 48 |
+
new_shape: Tuple[int, int]=(640, 640),
|
| 49 |
+
border_color: Tuple[int, int, int]=(114, 114, 114),
|
| 50 |
+
auto: bool=True,
|
| 51 |
+
scaleFill: bool=False,
|
| 52 |
+
scaleup: bool=True,
|
| 53 |
+
stride: int=32):
|
| 54 |
+
""" Resize input image.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
img (np.ndarray): input image.
|
| 58 |
+
new_shape (Tuple[int, int]): the shape of output image.
|
| 59 |
+
border_color: Tuple[int, int, int]: padding value.
|
| 60 |
+
auto (bool): resize input to minimum rectangle.
|
| 61 |
+
scaleFill (bool): stretching the input image.
|
| 62 |
+
scaleUp (bool): allow scale up the input image.
|
| 63 |
+
stride (int): stride of the model.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
# Resize and pad image while meeting stride-multiple constraints
|
| 67 |
+
shape = img.shape[:2] # current shape [height, width]
|
| 68 |
+
if isinstance(new_shape, int):
|
| 69 |
+
new_shape = (new_shape, new_shape)
|
| 70 |
+
|
| 71 |
+
# Scale ratio (new / old)
|
| 72 |
+
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
|
| 73 |
+
if not scaleup: # only scale down, do not scale up (for better test mAP)
|
| 74 |
+
r = min(r, 1.0)
|
| 75 |
+
|
| 76 |
+
# Compute padding
|
| 77 |
+
ratio = r, r # width, height ratios
|
| 78 |
+
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
|
| 79 |
+
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
|
| 80 |
+
if auto: # minimum rectangle
|
| 81 |
+
dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding
|
| 82 |
+
elif scaleFill: # stretch
|
| 83 |
+
dw, dh = 0.0, 0.0
|
| 84 |
+
new_unpad = (new_shape[1], new_shape[0])
|
| 85 |
+
ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios
|
| 86 |
+
|
| 87 |
+
dw /= 2 # divide padding into 2 sides
|
| 88 |
+
dh /= 2
|
| 89 |
+
|
| 90 |
+
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
|
| 91 |
+
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
|
| 92 |
+
|
| 93 |
+
if shape[::-1] != new_unpad: # resize
|
| 94 |
+
new_img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
|
| 95 |
+
else:
|
| 96 |
+
new_img = img
|
| 97 |
+
new_img = cv2.copyMakeBorder(new_img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=border_color) # add border
|
| 98 |
+
return new_img, ratio, (dw, dh)
|
| 99 |
+
|
| 100 |
+
def preprocess(self, input_data: np.ndarray):
|
| 101 |
+
""" Preprocess function for input data.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
input_data (np.ndarray): batch input image.
|
| 105 |
+
"""
|
| 106 |
+
tensor_data = []
|
| 107 |
+
if ((isinstance(input_data, np.ndarray)) and (len(input_data.shape) == 3)):
|
| 108 |
+
input_data = [input_data]
|
| 109 |
+
for i in range(len(input_data)):
|
| 110 |
+
img = input_data[i]
|
| 111 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 112 |
+
preprocessed_img, ratio, dwdh = self.letterbox(img, new_shape=self.input_shape[2:], **self.preprocess_cfg)
|
| 113 |
+
height, width = preprocessed_img.shape[0], preprocessed_img.shape[1]
|
| 114 |
+
if self.use_torch:
|
| 115 |
+
tensor_data.append(torch.from_numpy(preprocessed_img).to(self.device))
|
| 116 |
+
else:
|
| 117 |
+
tensor_data.append(preprocessed_img)
|
| 118 |
+
if self.use_torch:
|
| 119 |
+
tensor_data = torch.stack(tensor_data, dim=0)[:, :, :, [2, 1, 0]].permute(0, 3, 1, 2).float().contiguous()/255.0
|
| 120 |
+
else:
|
| 121 |
+
tensor_data = np.stack(tensor_data, axis=0)/255.0
|
| 122 |
+
|
| 123 |
+
return tensor_data, height, width, ratio[0], dwdh
|
| 124 |
+
|
| 125 |
+
def postprocess(self,
|
| 126 |
+
boxes: Union[torch.tensor,np.ndarray],
|
| 127 |
+
r: float,
|
| 128 |
+
dwdh: Tuple[float, float]):
|
| 129 |
+
""" Postprocess function for input data.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
boxes (torch.tensor or np.ndarray): output boxes.
|
| 133 |
+
r (float): ratio between model shape and input shape.
|
| 134 |
+
dwdh (Tuple[float, float]): top-left padding position.
|
| 135 |
+
"""
|
| 136 |
+
dwdh = dwdh * 2
|
| 137 |
+
if self.use_torch:
|
| 138 |
+
dwdh = torch.tensor(dwdh)
|
| 139 |
+
boxes -= dwdh
|
| 140 |
+
boxes /= r
|
| 141 |
+
return boxes
|
| 142 |
+
|
| 143 |
+
class YOLOv7TRT(TRT_Base, YOLOv7Base):
|
| 144 |
+
def __init__(self,
|
| 145 |
+
class_map_ids: Optional[Dict],
|
| 146 |
+
preprocess_cfg: Dict,
|
| 147 |
+
nms_agnostic_cfg: Dict,
|
| 148 |
+
img_shape: Tuple[int, int]=(640, 640),
|
| 149 |
+
batch_size: int=32,
|
| 150 |
+
model_path: str="",
|
| 151 |
+
device: str='0',):
|
| 152 |
+
""" YOLOv7 TRT class for inference, which is based on TRT_Base and YOLOv7Base.
|
| 153 |
+
"""
|
| 154 |
+
self.img_shape = img_shape
|
| 155 |
+
self.batch_size = batch_size
|
| 156 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 157 |
+
super().__init__(input_shape, model_path, device)
|
| 158 |
+
YOLOv7Base.__init__(self, class_map_ids=class_map_ids,
|
| 159 |
+
preprocess_cfg=preprocess_cfg,
|
| 160 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 161 |
+
use_torch=True)
|
| 162 |
+
|
| 163 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[np.ndarray]:
|
| 164 |
+
""" Batch inference function for batch input image.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
image_batch (np.ndarray): batch of input image.
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
tensor_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 171 |
+
self.change_runtime_dimension(input_shape=(len(tensor_data), 3, height, width))
|
| 172 |
+
self.model['binding_addrs']['images'] = int(tensor_data.data_ptr())
|
| 173 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 174 |
+
nums = self.model['bindings']['num_dets'].data.cpu()
|
| 175 |
+
boxes = self.model['bindings']['det_boxes'].data.cpu()
|
| 176 |
+
scores = self.model['bindings']['det_scores'].data.cpu()
|
| 177 |
+
classes = self.model['bindings']['det_classes'].data.cpu()
|
| 178 |
+
|
| 179 |
+
# Rearrange the classes idx.
|
| 180 |
+
new_classes = classes.clone()
|
| 181 |
+
if self.class_map_ids is not None:
|
| 182 |
+
for idx_s, idx_t in self.class_map_ids.items():
|
| 183 |
+
new_classes[classes == idx_s] = idx_t
|
| 184 |
+
boxes = self.postprocess(boxes, ratio, dwdh)
|
| 185 |
+
det_outputs = []
|
| 186 |
+
for idx in range(len(nums)):
|
| 187 |
+
num = nums[idx, 0]
|
| 188 |
+
frame_boxes = boxes[idx, :num]
|
| 189 |
+
frame_scores = scores[idx, :num]
|
| 190 |
+
frame_labels = new_classes[idx, :num]
|
| 191 |
+
result_boxes, keep = batched_nms(frame_boxes.float(), frame_scores.float(), frame_labels, nms_cfg=self.nms_agnostic_cfg)
|
| 192 |
+
det_outputs.append({"boxes": result_boxes,"labels": frame_labels[keep]})
|
| 193 |
+
return det_outputs
|
| 194 |
+
|
| 195 |
+
class YOLOv7ONNX(ONNX_Base, YOLOv7Base):
|
| 196 |
+
def __init__(self,
|
| 197 |
+
class_map_ids,
|
| 198 |
+
preprocess_cfg,
|
| 199 |
+
nms_agnostic_cfg,
|
| 200 |
+
model_path: str="",
|
| 201 |
+
device: str='0',):
|
| 202 |
+
""" YOLOv7 ONNX class for inference, which is based on ONNX_Base and YOLOv7Base.
|
| 203 |
+
"""
|
| 204 |
+
super().__init__(model_path,device)
|
| 205 |
+
YOLOv7Base.__init__(self, class_map_ids=class_map_ids,
|
| 206 |
+
preprocess_cfg=preprocess_cfg,
|
| 207 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 208 |
+
use_torch=False)
|
| 209 |
+
|
| 210 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[np.ndarray]:
|
| 211 |
+
numpy_array_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 212 |
+
results = super().infer_batch(numpy_array_data)
|
| 213 |
+
|
| 214 |
+
det_outputs = []
|
| 215 |
+
batch_size = len(results[0])
|
| 216 |
+
for idx in range(batch_size):
|
| 217 |
+
boxes = results[0][idx][1:5]
|
| 218 |
+
classes = results[0][idx][5]
|
| 219 |
+
scores = results[0][idx][-1]
|
| 220 |
+
boxes = self.postprocess(boxes, ratio, dwdh)
|
| 221 |
+
|
| 222 |
+
result_boxes, keep = batched_nms(boxes.float(), scores.float(), classes, nms_cfg=self.nms_agnostic_cfg)
|
| 223 |
+
det_outputs.append({"boxes": result_boxes,"labels": classes[keep]})
|
| 224 |
+
return det_outputs
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
|
models/models/__init__.py
ADDED
|
File without changes
|
models/models/base/__init__.py
ADDED
|
File without changes
|
models/models/base/onnx_base.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference for onnx model (.onnx)
|
| 2 |
+
from typing import List
|
| 3 |
+
import numpy as np
|
| 4 |
+
import onnxruntime as ort
|
| 5 |
+
import os, torch
|
| 6 |
+
|
| 7 |
+
class ONNX_Base():
|
| 8 |
+
def __init__(self,
|
| 9 |
+
input_shape,
|
| 10 |
+
model_path: str,
|
| 11 |
+
device: str='0'):
|
| 12 |
+
self.input_shape = input_shape
|
| 13 |
+
self.model_path = model_path
|
| 14 |
+
self.device = self.select_device(device)
|
| 15 |
+
self.session = self.create_session(model_path)
|
| 16 |
+
|
| 17 |
+
def create_session(self, model_path: str) -> ort.InferenceSession:
|
| 18 |
+
"""_summary_
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
model_path (_type_): _description_
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
_type_: _description_
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
providers = ['CPUExecutionProvider']
|
| 28 |
+
if torch.cuda.is_available():
|
| 29 |
+
providers.insert(0, 'CUDAExecutionProvider')
|
| 30 |
+
ort_session = ort.InferenceSession(model_path, providers=providers)
|
| 31 |
+
return ort_session
|
| 32 |
+
|
| 33 |
+
def select_device(self, device: str)->torch.device:
|
| 34 |
+
""" Select device to be used for inference.
|
| 35 |
+
Args:
|
| 36 |
+
param device: 'cpu' or '0' or '0,1,2,3'
|
| 37 |
+
Return:
|
| 38 |
+
torch.device
|
| 39 |
+
"""
|
| 40 |
+
cpu = device.lower() == "cpu"
|
| 41 |
+
if cpu:
|
| 42 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
| 43 |
+
return torch.device("cpu")
|
| 44 |
+
else:
|
| 45 |
+
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested'
|
| 46 |
+
os.environ['CUDA_VISBILE_DEVICES'] = device
|
| 47 |
+
torch.cuda.set_device(int(device))
|
| 48 |
+
return torch.device(f"cuda:{device}")
|
| 49 |
+
|
| 50 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[np.ndarray]:
|
| 51 |
+
""" Inference for onnx model.
|
| 52 |
+
Args:
|
| 53 |
+
param image_batch: (batch_size, height, width, channels)
|
| 54 |
+
Return:
|
| 55 |
+
results: List[np.ndarray]
|
| 56 |
+
"""
|
| 57 |
+
input_name = self.session.get_inputs()[0].name
|
| 58 |
+
results = self.session.run(None, {input_name: image_batch})
|
| 59 |
+
return results
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
models/models/base/trt_base.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from typing import Tuple, Dict,List
|
| 3 |
+
import tensorrt as trt
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import time
|
| 7 |
+
import os
|
| 8 |
+
from collections import OrderedDict, namedtuple
|
| 9 |
+
|
| 10 |
+
class TRT_Base():
|
| 11 |
+
def __init__(self,
|
| 12 |
+
input_shape: Tuple[int, int, int],
|
| 13 |
+
model_path: str,
|
| 14 |
+
device: str='0'):
|
| 15 |
+
""" Tensor RT base class for inference.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 19 |
+
model_path (str): path to the model.trt
|
| 20 |
+
device (str, optional): CUDA device. Defaults to '0'.
|
| 21 |
+
"""
|
| 22 |
+
self.input_shape = input_shape
|
| 23 |
+
self.model_path = model_path
|
| 24 |
+
self.device = self.select_device(device)
|
| 25 |
+
self.init_model()
|
| 26 |
+
|
| 27 |
+
def select_device(self, device: str)->torch.device:
|
| 28 |
+
""" Select device to be used for inference.
|
| 29 |
+
Args:
|
| 30 |
+
param device: 'cpu' or '0' or '0,1,2,3'
|
| 31 |
+
Return:
|
| 32 |
+
torch.device
|
| 33 |
+
"""
|
| 34 |
+
cpu = device.lower() == "cpu"
|
| 35 |
+
if cpu:
|
| 36 |
+
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
|
| 37 |
+
return torch.device("cpu")
|
| 38 |
+
else:
|
| 39 |
+
assert torch.cuda.is_available(), f'CUDA unavailable, invalid device {device} requested'
|
| 40 |
+
torch.cuda.set_device(int(device))
|
| 41 |
+
return torch.device("cuda")
|
| 42 |
+
|
| 43 |
+
def init_model(self):
|
| 44 |
+
""" Initialize TensorRT engine and context."""
|
| 45 |
+
logger = trt.Logger(trt.Logger.INFO)
|
| 46 |
+
trt.init_libnvinfer_plugins(logger, namespace="")
|
| 47 |
+
with open(self.model_path, 'rb') as f, trt.Runtime(logger) as runtime:
|
| 48 |
+
engine = runtime.deserialize_cuda_engine(f.read())
|
| 49 |
+
context = engine.create_execution_context()
|
| 50 |
+
self.model = {
|
| 51 |
+
"engine": engine,
|
| 52 |
+
"context": context
|
| 53 |
+
}
|
| 54 |
+
bindings, binding_addrs = self.get_bindings(input_shape=self.input_shape)
|
| 55 |
+
input_names = [binding_name for binding_name in binding_addrs.keys() if (self.model["engine"].binding_is_input(binding_name))]
|
| 56 |
+
|
| 57 |
+
for _ in range(10):
|
| 58 |
+
for name in input_names:
|
| 59 |
+
binding_addrs[name] = int(torch.randn(bindings[name].shape).to(self.device).data_ptr())
|
| 60 |
+
context.execute_v2(list(binding_addrs.values()))
|
| 61 |
+
self.model.update({
|
| 62 |
+
'binding_addrs': binding_addrs,
|
| 63 |
+
'bindings': bindings,
|
| 64 |
+
'rt_shapes': self.input_shape
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
def get_bindings(self, input_shape: Tuple[int, int, int]):
|
| 68 |
+
""" Get bindings and binding addresses for TensorRT engine.
|
| 69 |
+
Args:
|
| 70 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 71 |
+
"""
|
| 72 |
+
self.model["context"].set_binding_shape(0, input_shape)
|
| 73 |
+
bindings = OrderedDict()
|
| 74 |
+
Binding = namedtuple('Binding', ('name', 'dtype', 'shape', 'data', 'ptr'))
|
| 75 |
+
for index in range(self.model["engine"].num_bindings):
|
| 76 |
+
name = self.model["engine"].get_binding_name(index)
|
| 77 |
+
dtype = trt.nptype(self.model["engine"].get_binding_dtype(index))
|
| 78 |
+
shape = tuple(self.model["context"].get_binding_shape(index))
|
| 79 |
+
data = torch.from_numpy(np.empty(shape, dtype=np.dtype(dtype))).to(self.device)
|
| 80 |
+
bindings[name] = Binding(name, dtype, shape, data, int(data.data_ptr()))
|
| 81 |
+
binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items())
|
| 82 |
+
return bindings, binding_addrs
|
| 83 |
+
|
| 84 |
+
def change_runtime_dimension(self, input_shape: Tuple[int, int, int]):
|
| 85 |
+
""" Support inference with Dynamic shape.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
input_shape (Tuple[int, int, int]): image size (3, H, W)
|
| 89 |
+
"""
|
| 90 |
+
if (input_shape == self.model["rt_shapes"]): return
|
| 91 |
+
bindings, binding_addrs = self.get_bindings(input_shape)
|
| 92 |
+
self.model['binding_addrs'] = binding_addrs
|
| 93 |
+
self.model['bindings'] = bindings
|
| 94 |
+
self.model['rt_shapes'] = input_shape
|
| 95 |
+
|
| 96 |
+
|
models/models/detectors/__init__.py
ADDED
|
File without changes
|
models/models/detectors/mmyolov8.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Tuple
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
from .yolov7 import YOLOBase
|
| 5 |
+
from models.base.trt_base import TRT_Base
|
| 6 |
+
from models.base.onnx_base import ONNX_Base
|
| 7 |
+
|
| 8 |
+
class MMYOLOv8TRT(TRT_Base, YOLOBase):
|
| 9 |
+
def __init__(self,
|
| 10 |
+
preprocess_cfg: Dict=dict(
|
| 11 |
+
border_color=(114, 114, 114),
|
| 12 |
+
auto=False,
|
| 13 |
+
scaleFill=True,
|
| 14 |
+
scaleup=True,
|
| 15 |
+
stride=32),
|
| 16 |
+
nms_agnostic_cfg: Dict=dict(
|
| 17 |
+
type='nms',
|
| 18 |
+
iou_threshold=0.9,
|
| 19 |
+
class_agnostic=True),
|
| 20 |
+
score_thr=0.1,
|
| 21 |
+
img_shape: Tuple[int, int]=(640, 640),
|
| 22 |
+
batch_size: int=32,
|
| 23 |
+
model_path: str="",
|
| 24 |
+
device: str='0',):
|
| 25 |
+
""" YOLOv8 TRT class for inference, which is based on TRT_Base and YOLOBase.
|
| 26 |
+
"""
|
| 27 |
+
self.img_shape = img_shape
|
| 28 |
+
self.batch_size = batch_size
|
| 29 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 30 |
+
super().__init__(input_shape, model_path, device)
|
| 31 |
+
YOLOBase.__init__(self, preprocess_cfg=preprocess_cfg,
|
| 32 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 33 |
+
score_thr=score_thr,
|
| 34 |
+
use_torch=True)
|
| 35 |
+
|
| 36 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[Dict]:
|
| 37 |
+
""" Batch inference function for batch input image.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
image_batch (np.ndarray): batch of input image.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
tensor_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 44 |
+
self.change_runtime_dimension(input_shape=(len(tensor_data), 3, height, width))
|
| 45 |
+
self.model['binding_addrs']['input'] = int(tensor_data.data_ptr())
|
| 46 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 47 |
+
dets = self.model['bindings']['dets'].data.cpu()
|
| 48 |
+
classes = self.model['bindings']['labels'].data.cpu()
|
| 49 |
+
boxes = dets[:,:,:4]
|
| 50 |
+
scores = dets[:,:,4]
|
| 51 |
+
|
| 52 |
+
return self.post_process(boxes, scores, classes, ratio, dwdh)
|
| 53 |
+
|
| 54 |
+
class MMYOLOv8ONNX(ONNX_Base, YOLOBase):
|
| 55 |
+
def __init__(self,
|
| 56 |
+
preprocess_cfg,
|
| 57 |
+
nms_agnostic_cfg,
|
| 58 |
+
score_thr=0.1,
|
| 59 |
+
img_shape: Tuple[int, int]=(640, 640),
|
| 60 |
+
batch_size: int=32,
|
| 61 |
+
model_path: str="",
|
| 62 |
+
device: str='0',):
|
| 63 |
+
""" YOLOv7 ONNX class for inference, which is based on ONNX_Base and YOLOBase.
|
| 64 |
+
"""
|
| 65 |
+
self.img_shape = img_shape
|
| 66 |
+
self.batch_size = batch_size
|
| 67 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 68 |
+
super().__init__(input_shape, model_path, device)
|
| 69 |
+
YOLOBase.__init__(self,
|
| 70 |
+
preprocess_cfg=preprocess_cfg,
|
| 71 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 72 |
+
score_thr=score_thr,
|
| 73 |
+
use_torch=False)
|
| 74 |
+
|
| 75 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[Dict]:
|
| 76 |
+
|
| 77 |
+
numpy_array_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 78 |
+
numpy_array_data = numpy_array_data.astype(np.float32)
|
| 79 |
+
results = super().infer_batch(numpy_array_data)
|
| 80 |
+
dets, classes = results
|
| 81 |
+
dets = torch.from_numpy(dets)
|
| 82 |
+
classes = torch.from_numpy(classes)
|
| 83 |
+
boxes = dets[:,:,:4]
|
| 84 |
+
scores = dets[:,:,4]
|
| 85 |
+
|
| 86 |
+
return self.post_process(boxes, scores, classes, ratio, dwdh)
|
models/models/detectors/yolov7.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Tuple, Union
|
| 2 |
+
import torch, cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from models.base.trt_base import TRT_Base
|
| 6 |
+
from models.base.onnx_base import ONNX_Base
|
| 7 |
+
from mmcv.ops.nms import batched_nms, nms
|
| 8 |
+
|
| 9 |
+
class YOLOBase():
|
| 10 |
+
def __init__(self,
|
| 11 |
+
preprocess_cfg: Dict=dict(
|
| 12 |
+
border_color=(114, 114, 114),
|
| 13 |
+
auto=False,
|
| 14 |
+
scaleFill=True,
|
| 15 |
+
scaleup=True,
|
| 16 |
+
stride=32),
|
| 17 |
+
nms_agnostic_cfg: Dict=dict(
|
| 18 |
+
type='nms',
|
| 19 |
+
iou_threshold=0.9,
|
| 20 |
+
class_agnostic=True),
|
| 21 |
+
score_thr=0.1,
|
| 22 |
+
use_torch: bool=False,
|
| 23 |
+
):
|
| 24 |
+
""" This base-class has preprocess and postprocess function for YOLO model.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
preprocess_cfg (Dict):
|
| 28 |
+
- border_color (Tuple[int, int, int]): padding value.
|
| 29 |
+
- auto (bool): resize input to minimum rectangle.
|
| 30 |
+
- scaleFill (bool): stretching the input image.
|
| 31 |
+
- scaleUp (bool): allow scale up the input image.
|
| 32 |
+
- stride (int): stride of the model.
|
| 33 |
+
nms_agnostic_cfg (Dict):
|
| 34 |
+
- type (str): nms type (nms, softnms).
|
| 35 |
+
- iou_threshold (float): IoU threshold of NMS.
|
| 36 |
+
- class_agnostic (bool): enable class-agnostic NMS instead of NMS for each class.
|
| 37 |
+
use_torch (bool): use torch tensor or numpy array in preprocess and postprocess function.
|
| 38 |
+
"""
|
| 39 |
+
self.preprocess_cfg=preprocess_cfg
|
| 40 |
+
self.nms_agnostic_cfg=nms_agnostic_cfg
|
| 41 |
+
self.use_torch= use_torch
|
| 42 |
+
self.score_thr = score_thr
|
| 43 |
+
|
| 44 |
+
def letterbox(self,
|
| 45 |
+
img: np.ndarray,
|
| 46 |
+
new_shape: Tuple[int, int]=(640, 640),
|
| 47 |
+
border_color: Tuple[int, int, int]=(114, 114, 114),
|
| 48 |
+
auto: bool=True,
|
| 49 |
+
scaleFill: bool=False,
|
| 50 |
+
scaleup: bool=True,
|
| 51 |
+
stride: int=32):
|
| 52 |
+
""" Resize input image.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
img (np.ndarray): input image.
|
| 56 |
+
new_shape (Tuple[int, int]): the shape of output image.
|
| 57 |
+
border_color: Tuple[int, int, int]: padding value.
|
| 58 |
+
auto (bool): resize input to minimum rectangle.
|
| 59 |
+
scaleFill (bool): stretching the input image.
|
| 60 |
+
scaleUp (bool): allow scale up the input image.
|
| 61 |
+
stride (int): stride of the model.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
# Resize and pad image while meeting stride-multiple constraints
|
| 65 |
+
shape = img.shape[:2] # current shape [height, width]
|
| 66 |
+
if isinstance(new_shape, int):
|
| 67 |
+
new_shape = (new_shape, new_shape)
|
| 68 |
+
|
| 69 |
+
# Scale ratio (new / old)
|
| 70 |
+
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
|
| 71 |
+
if not scaleup: # only scale down, do not scale up (for better test mAP)
|
| 72 |
+
r = min(r, 1.0)
|
| 73 |
+
|
| 74 |
+
# Compute padding
|
| 75 |
+
ratio = r, r # width, height ratios
|
| 76 |
+
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
|
| 77 |
+
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
|
| 78 |
+
if auto: # minimum rectangle
|
| 79 |
+
dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding
|
| 80 |
+
elif scaleFill: # stretch
|
| 81 |
+
dw, dh = 0.0, 0.0
|
| 82 |
+
new_unpad = (new_shape[1], new_shape[0])
|
| 83 |
+
ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios
|
| 84 |
+
|
| 85 |
+
dw /= 2 # divide padding into 2 sides
|
| 86 |
+
dh /= 2
|
| 87 |
+
|
| 88 |
+
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
|
| 89 |
+
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
|
| 90 |
+
|
| 91 |
+
if shape[::-1] != new_unpad: # resize
|
| 92 |
+
new_img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
|
| 93 |
+
else:
|
| 94 |
+
new_img = img
|
| 95 |
+
new_img = cv2.copyMakeBorder(new_img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=border_color) # add border
|
| 96 |
+
return new_img, ratio, (dw, dh)
|
| 97 |
+
|
| 98 |
+
def preprocess(self, input_data: np.ndarray):
|
| 99 |
+
""" Preprocess function for input data.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
input_data (np.ndarray): batch input image.
|
| 103 |
+
"""
|
| 104 |
+
tensor_data = []
|
| 105 |
+
if ((isinstance(input_data, np.ndarray)) and (len(input_data.shape) == 3)):
|
| 106 |
+
input_data = [input_data]
|
| 107 |
+
for i in range(len(input_data)):
|
| 108 |
+
img = input_data[i]
|
| 109 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 110 |
+
preprocessed_img, ratio, dwdh = self.letterbox(img, new_shape=self.input_shape[2:], **self.preprocess_cfg)
|
| 111 |
+
height, width = preprocessed_img.shape[0], preprocessed_img.shape[1]
|
| 112 |
+
if self.use_torch:
|
| 113 |
+
tensor_data.append(torch.from_numpy(preprocessed_img).to(self.device))
|
| 114 |
+
else:
|
| 115 |
+
tensor_data.append(preprocessed_img)
|
| 116 |
+
if self.use_torch:
|
| 117 |
+
tensor_data = torch.stack(tensor_data, dim=0).permute(0, 3, 1, 2).float().contiguous()/255.0
|
| 118 |
+
else:
|
| 119 |
+
tensor_data = np.stack(tensor_data, axis=0).transpose(0, 3, 1, 2)/255.0
|
| 120 |
+
|
| 121 |
+
return tensor_data, height, width, ratio[0], dwdh
|
| 122 |
+
|
| 123 |
+
def scale_boxes(self,
|
| 124 |
+
boxes: Union[torch.Tensor,np.ndarray],
|
| 125 |
+
r: float,
|
| 126 |
+
dwdh: Tuple[float, float]) -> torch.Tensor:
|
| 127 |
+
""" Scale predicted boxes to the original image shape.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
boxes (torch.tensor or np.ndarray): output boxes.
|
| 131 |
+
r (float): ratio between model shape and input shape.
|
| 132 |
+
dwdh (Tuple[float, float]): top-left padding position.
|
| 133 |
+
"""
|
| 134 |
+
dwdh = dwdh * 2
|
| 135 |
+
if isinstance(boxes, torch.Tensor):
|
| 136 |
+
dwdh = torch.tensor(dwdh)
|
| 137 |
+
boxes -= dwdh
|
| 138 |
+
boxes /= r
|
| 139 |
+
return boxes
|
| 140 |
+
|
| 141 |
+
def post_process(self,
|
| 142 |
+
boxes: torch.Tensor,
|
| 143 |
+
scores: torch.Tensor,
|
| 144 |
+
classes: torch.Tensor,
|
| 145 |
+
ratio: float,
|
| 146 |
+
dwdh: Tuple[int,int]) -> List[Dict]:
|
| 147 |
+
""" Postprocess function for input data.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
boxes (torch.Tensor): output boxes of shape [N_box, 4].
|
| 151 |
+
scores (torch.Tensor): confidence scores of shape [N_box,].
|
| 152 |
+
classes (torch.Tensor): class ids of shape [N_box,].
|
| 153 |
+
ratio (float): ratio between original image shape and model's input shape.
|
| 154 |
+
dwdh (Tuple[int,int]): padding margin output from letterbox function.
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
List[Dict]: _description_
|
| 158 |
+
"""
|
| 159 |
+
boxes = self.scale_boxes(boxes, ratio, dwdh)
|
| 160 |
+
det_outputs = []
|
| 161 |
+
batch_size = len(boxes)
|
| 162 |
+
for idx in range(batch_size):
|
| 163 |
+
frame_boxes = boxes[idx]
|
| 164 |
+
frame_scores = scores[idx]
|
| 165 |
+
frame_labels = classes[idx]
|
| 166 |
+
# Filter out confidence scores below threshold
|
| 167 |
+
index = frame_scores > self.score_thr
|
| 168 |
+
frame_boxes = frame_boxes[index]
|
| 169 |
+
frame_scores = frame_scores[index]
|
| 170 |
+
frame_labels = frame_labels[index]
|
| 171 |
+
# Use NMS to suppress class-agnostic boxes
|
| 172 |
+
result_boxes, keep = batched_nms(frame_boxes.float(), frame_scores.float(), frame_labels, nms_cfg=self.nms_agnostic_cfg)
|
| 173 |
+
det_outputs.append({"boxes": result_boxes,"labels": frame_labels[keep]})
|
| 174 |
+
return det_outputs
|
| 175 |
+
|
| 176 |
+
class YOLOv7TRT(TRT_Base, YOLOBase):
|
| 177 |
+
def __init__(self,
|
| 178 |
+
preprocess_cfg: Dict,
|
| 179 |
+
nms_agnostic_cfg: Dict,
|
| 180 |
+
score_thr=0.1,
|
| 181 |
+
img_shape: Tuple[int, int]=(640, 640),
|
| 182 |
+
batch_size: int=32,
|
| 183 |
+
model_path: str="",
|
| 184 |
+
device: str='0',):
|
| 185 |
+
""" YOLOv7 TRT class for inference, which is based on TRT_Base and YOLOBase.
|
| 186 |
+
"""
|
| 187 |
+
self.img_shape = img_shape
|
| 188 |
+
self.batch_size = batch_size
|
| 189 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 190 |
+
super().__init__(input_shape, model_path, device)
|
| 191 |
+
YOLOBase.__init__(self, preprocess_cfg=preprocess_cfg,
|
| 192 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 193 |
+
score_thr=score_thr,
|
| 194 |
+
use_torch=True)
|
| 195 |
+
|
| 196 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[Dict]:
|
| 197 |
+
""" Batch inference function for batch input image.
|
| 198 |
+
|
| 199 |
+
Args:
|
| 200 |
+
image_batch (np.ndarray): batch of input image.
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
tensor_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 204 |
+
self.change_runtime_dimension(input_shape=(len(tensor_data), 3, height, width))
|
| 205 |
+
self.model['binding_addrs']['images'] = int(tensor_data.data_ptr())
|
| 206 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 207 |
+
nums = self.model['bindings']['num_dets'].data.cpu()
|
| 208 |
+
boxes = self.model['bindings']['det_boxes'].data.cpu()
|
| 209 |
+
scores = self.model['bindings']['det_scores'].data.cpu()
|
| 210 |
+
classes = self.model['bindings']['det_classes'].data.cpu()
|
| 211 |
+
|
| 212 |
+
return self.post_process(boxes, scores, classes, ratio, dwdh)
|
| 213 |
+
|
| 214 |
+
class YOLOv7ONNX(ONNX_Base, YOLOBase):
|
| 215 |
+
def __init__(self,
|
| 216 |
+
preprocess_cfg,
|
| 217 |
+
nms_agnostic_cfg,
|
| 218 |
+
score_thr=0.1,
|
| 219 |
+
img_shape: Tuple[int, int]=(640, 640),
|
| 220 |
+
batch_size: int=32,
|
| 221 |
+
model_path: str="",
|
| 222 |
+
device: str='0',):
|
| 223 |
+
""" YOLOv7 ONNX class for inference, which is based on ONNX_Base and YOLOBase.
|
| 224 |
+
"""
|
| 225 |
+
self.img_shape = img_shape
|
| 226 |
+
self.batch_size = batch_size
|
| 227 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 228 |
+
super().__init__(input_shape, model_path, device)
|
| 229 |
+
YOLOBase.__init__(self,
|
| 230 |
+
preprocess_cfg=preprocess_cfg,
|
| 231 |
+
nms_agnostic_cfg=nms_agnostic_cfg,
|
| 232 |
+
score_thr=score_thr,
|
| 233 |
+
use_torch=False)
|
| 234 |
+
|
| 235 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[Dict]:
|
| 236 |
+
|
| 237 |
+
numpy_array_data, height, width, ratio, dwdh = self.preprocess(image_batch)
|
| 238 |
+
numpy_array_data = numpy_array_data.astype(np.float32)
|
| 239 |
+
results = super().infer_batch(numpy_array_data)
|
| 240 |
+
num_dets, boxes, scores, classes = results
|
| 241 |
+
boxes = torch.from_numpy(boxes)
|
| 242 |
+
scores = torch.from_numpy(scores)
|
| 243 |
+
classes = torch.from_numpy(classes)
|
| 244 |
+
return self.post_process(boxes, scores, classes, ratio, dwdh)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
|
models/models/engine/__init__.py
ADDED
|
File without changes
|
models/models/engine/threading_func.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from queue import Queue, Full, Empty
|
| 2 |
+
from threading import Event
|
| 3 |
+
from mmcv import VideoReader
|
| 4 |
+
import logging
|
| 5 |
+
from gradio import Progress
|
| 6 |
+
from models.trackers.byte_track import BYTETracker
|
| 7 |
+
import torch
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
def queue_clear(q: Queue):
|
| 11 |
+
""" Clear all items in the queue.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
q (Queue): input queue.
|
| 15 |
+
"""
|
| 16 |
+
with q.mutex: q.queue.clear()
|
| 17 |
+
|
| 18 |
+
def queue_get(q: Queue, eStop: Event, retry_interval=1, item_idx=None, default_item=None):
|
| 19 |
+
"""wrapper for queue.get() with timeout, retry and event stop.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
q (Queue): input queue.
|
| 23 |
+
eStop (Event): event to stop the thread.
|
| 24 |
+
retry_interval (int, optional): time to wait before retry to get the item. Defaults to 1 second.
|
| 25 |
+
item_idx (_type_, optional): index of item to get. This is used for logging information. Defaults to None.
|
| 26 |
+
default_item (_type_, optional): default item to return if error or early stop. Defaults to None.
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
any: item in the queue.
|
| 30 |
+
"""
|
| 31 |
+
if not q.empty():
|
| 32 |
+
return q.get()
|
| 33 |
+
|
| 34 |
+
while not eStop.is_set():
|
| 35 |
+
try:
|
| 36 |
+
item = q.get(timeout=retry_interval)
|
| 37 |
+
return item
|
| 38 |
+
except Empty:
|
| 39 |
+
if item_idx is not None:
|
| 40 |
+
logging.info(f"Waiting to get item {item_idx}")
|
| 41 |
+
if item_idx is not None:
|
| 42 |
+
logging.info(f"Early Stop. Return Default item at iter {item_idx}")
|
| 43 |
+
return default_item
|
| 44 |
+
|
| 45 |
+
def queue_put(q: Queue, item, eStop: Event, retry_interval=1, item_idx=None):
|
| 46 |
+
""" wrapper for queue.put() with timeout, retry and event stop.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
q (Queue): input queue.
|
| 50 |
+
item (_type_): item to put in the queue.
|
| 51 |
+
eStop (Event): event to stop the thread.
|
| 52 |
+
retry_interval (int, optional): time to wait before retry to put the item. Defaults to 1 second.
|
| 53 |
+
item_idx (_type_, optional): index of item to put. This is used for logging information. Defaults to None.
|
| 54 |
+
"""
|
| 55 |
+
if not q.full():
|
| 56 |
+
q.put(item)
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
while not eStop.is_set():
|
| 60 |
+
try:
|
| 61 |
+
q.put(item, timeout=retry_interval)
|
| 62 |
+
return
|
| 63 |
+
except Full:
|
| 64 |
+
if item_idx is not None:
|
| 65 |
+
logging.info(f"Waiting to put item at {item_idx}")
|
| 66 |
+
if item_idx is not None:
|
| 67 |
+
logging.info(f"Early Stop. No item is put at iter {item_idx}")
|
| 68 |
+
|
| 69 |
+
def batch_extract_thread(video_path: str,
|
| 70 |
+
img_batch_queue: Queue,
|
| 71 |
+
vis_img_batch_queue: Queue,
|
| 72 |
+
eStop: Event,
|
| 73 |
+
batch_size=32):
|
| 74 |
+
"""Thread function to extract a batch of frames from video and put it to img_batch_queue and vis_img_batch_queue.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
video_path (str): input video path.
|
| 78 |
+
img_batch_queue (Queue): output queue for batch of frames, used for processing.
|
| 79 |
+
vis_img_batch_queue (Queue): output queue for batch of frames, used for visualization.
|
| 80 |
+
eStop (Event): event to stop the thread.
|
| 81 |
+
batch_size (int, optional): number of images in a batch. Defaults to 32.
|
| 82 |
+
"""
|
| 83 |
+
logging.info("Start Batch Extract Thread")
|
| 84 |
+
vidcap = VideoReader(video_path)
|
| 85 |
+
vis_img_batch_queue.put([vidcap.fps, vidcap.width, vidcap.height, len(vidcap)])
|
| 86 |
+
start_frame_idx = 0
|
| 87 |
+
last_frame_idx = len(vidcap)
|
| 88 |
+
end_frame_idx = start_frame_idx
|
| 89 |
+
|
| 90 |
+
while (start_frame_idx < last_frame_idx):
|
| 91 |
+
if eStop.is_set(): break
|
| 92 |
+
end_frame_idx = min(start_frame_idx + batch_size, last_frame_idx)
|
| 93 |
+
img_batch = []
|
| 94 |
+
for frame_idx in range(start_frame_idx, end_frame_idx):
|
| 95 |
+
img = vidcap[frame_idx]
|
| 96 |
+
if (img is None):
|
| 97 |
+
break
|
| 98 |
+
img_batch.append(img)
|
| 99 |
+
if (len(img_batch) == 0):
|
| 100 |
+
break
|
| 101 |
+
item_data = [start_frame_idx, img_batch]
|
| 102 |
+
queue_put(img_batch_queue, item_data, eStop)
|
| 103 |
+
queue_put(vis_img_batch_queue, item_data , eStop)
|
| 104 |
+
start_frame_idx = end_frame_idx
|
| 105 |
+
|
| 106 |
+
if eStop.is_set():
|
| 107 |
+
queue_clear(img_batch_queue)
|
| 108 |
+
queue_clear(vis_img_batch_queue)
|
| 109 |
+
else:
|
| 110 |
+
logging.info(f"Finish batch_extract_thread for video_file {video_path} at end_frame_idx {end_frame_idx}.")
|
| 111 |
+
img_batch_queue.put(None)
|
| 112 |
+
vis_img_batch_queue.put(None)
|
| 113 |
+
|
| 114 |
+
def detect_thread(obj_detector,
|
| 115 |
+
img_batch_queue: Queue,
|
| 116 |
+
det_queue: Queue,
|
| 117 |
+
eStop: Event,
|
| 118 |
+
put_img_batch: bool=False):
|
| 119 |
+
""" detect_thread function to run detection on a batch of frames.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
obj_detector (_type_): object detector, for example YOLOV7TRT/-ONXX.
|
| 123 |
+
img_batch_queue (Queue): input queue for batch of frames, which is the output from batch_extract_thread.
|
| 124 |
+
det_queue (Queue): output queue for detection results.
|
| 125 |
+
eStop (Event): event to stop the thread.
|
| 126 |
+
put_img_batch (bool, optional): If True, the input image batch will also put tp det_queue.
|
| 127 |
+
This is often used for later step that require images of detected objects, such Human Pose or ReID.
|
| 128 |
+
Defaults to False.
|
| 129 |
+
"""
|
| 130 |
+
logging.info("Start Detection Thread")
|
| 131 |
+
item = img_batch_queue.get()
|
| 132 |
+
start_frame_idx = -1
|
| 133 |
+
while item is not None:
|
| 134 |
+
if eStop.is_set(): break
|
| 135 |
+
start_frame_idx, img_batch = item
|
| 136 |
+
logging.info(f"Run detection at frame idx: {start_frame_idx}")
|
| 137 |
+
try:
|
| 138 |
+
det_result = obj_detector.infer_batch(img_batch)
|
| 139 |
+
item_data = [start_frame_idx, det_result]
|
| 140 |
+
if (put_img_batch):
|
| 141 |
+
item_data.append(img_batch)
|
| 142 |
+
queue_put(det_queue, item_data, eStop)
|
| 143 |
+
except Exception as e:
|
| 144 |
+
error_msg=[501, f"Error when running detection at frame idx: {start_frame_idx}]. "]
|
| 145 |
+
log_error_message = f"{error_msg[1]}. Error {e}"
|
| 146 |
+
logging.exception(log_error_message)
|
| 147 |
+
eStop.set()
|
| 148 |
+
break
|
| 149 |
+
item = img_batch_queue.get()
|
| 150 |
+
|
| 151 |
+
# Finish this thread.
|
| 152 |
+
if eStop.is_set():
|
| 153 |
+
logging.warning(f"Early stop detect_thread at start_frame_idx {start_frame_idx}")
|
| 154 |
+
queue_clear(det_queue)
|
| 155 |
+
else:
|
| 156 |
+
logging.info(f"Finish detect_thread at start_frame_idx {start_frame_idx}.")
|
| 157 |
+
det_queue.put(None)
|
| 158 |
+
|
| 159 |
+
def bytetrack_thread(tracker_cfg, det_queue: Queue, track_queue: Queue, eStop: Event, conf_thres: float):
|
| 160 |
+
logging.info("Start Tracking Thread")
|
| 161 |
+
tracker = BYTETracker(
|
| 162 |
+
**tracker_cfg
|
| 163 |
+
)
|
| 164 |
+
item = det_queue.get()
|
| 165 |
+
start_frame_idx = -1
|
| 166 |
+
|
| 167 |
+
while item is not None:
|
| 168 |
+
if eStop.is_set():break
|
| 169 |
+
start_frame_idx, det_result = item
|
| 170 |
+
|
| 171 |
+
if isinstance(det_result[0]['boxes'],np.ndarray):
|
| 172 |
+
det_result = [{key:torch.from_numpy(value) for key,value in dict_det.items()} for dict_det in det_result]
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
track_result = tracker.track_batch(start_frame_idx,det_result,conf_thres)
|
| 176 |
+
except Exception as e:
|
| 177 |
+
error_msg=[501,f"Error when running tracking at start_frame_idx {start_frame_idx}: {e}"]
|
| 178 |
+
log_error_message = f"Error {error_msg[0]}: {error_msg[1]}"
|
| 179 |
+
logging.error(log_error_message)
|
| 180 |
+
eStop.set()
|
| 181 |
+
break
|
| 182 |
+
queue_put(track_queue, [start_frame_idx, track_result], eStop)
|
| 183 |
+
item = det_queue.get()
|
| 184 |
+
|
| 185 |
+
# Finish this thread
|
| 186 |
+
if eStop.is_set():
|
| 187 |
+
logging.warning(f"Early stop at start_frame_idx {start_frame_idx}.")
|
| 188 |
+
queue_clear(track_queue)
|
| 189 |
+
else:
|
| 190 |
+
logging.info(f"Finish track_thread.")
|
| 191 |
+
track_queue.put(None)
|
| 192 |
+
|
| 193 |
+
def update_progress_thread(visualize_queue: Queue, progress: Progress, eStop: Event):
|
| 194 |
+
"""Show the progress of the video processing on Gradio, measured by the number of frames visualized.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
visualize_queue (Queue): input queue for batch of frames, which is the output from batch_extract_thread.
|
| 198 |
+
progress (Progress): Gradio progress bar.
|
| 199 |
+
eStop (Event): event to stop the thread.
|
| 200 |
+
"""
|
| 201 |
+
|
| 202 |
+
fps, width, height, total_num_frames = visualize_queue.get()
|
| 203 |
+
progress(0, desc="Starting...")
|
| 204 |
+
start_frame_idx = -1
|
| 205 |
+
for frame_idx in progress.tqdm(range(total_num_frames), total=total_num_frames):
|
| 206 |
+
item = visualize_queue.get()
|
| 207 |
+
if (item is None):
|
| 208 |
+
break
|
| 209 |
+
start_frame_idx = item
|
| 210 |
+
if (start_frame_idx != frame_idx):
|
| 211 |
+
error_msg=[501, f"Error when runing update progress at start_frame_idx {start_frame_idx}. "]
|
| 212 |
+
log_error_message = f"Error {error_msg[0]}: {error_msg[1]}"
|
| 213 |
+
logging.error(log_error_message)
|
| 214 |
+
eStop.set()
|
| 215 |
+
break
|
| 216 |
+
|
| 217 |
+
# Finish this thread
|
| 218 |
+
if eStop.is_set():
|
| 219 |
+
logging.warning(f"Early stop at start_frame_idx {start_frame_idx}")
|
| 220 |
+
else:
|
| 221 |
+
logging.info(f"Finish update_progress_thread.")
|
models/models/engine/utils.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import cv2, torch
|
| 3 |
+
|
| 4 |
+
from typing import Tuple, Dict, List
|
| 5 |
+
|
| 6 |
+
def bbox_xyxy2cs(bbox: np.ndarray,
|
| 7 |
+
padding: float = 1.) -> Tuple[np.ndarray, np.ndarray]:
|
| 8 |
+
"""Transform the bbox format from (x,y,w,h) into (center, scale)
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
bbox (ndarray): Bounding box(es) in shape (4,) or (n, 4), formatted
|
| 12 |
+
as (left, top, right, bottom)
|
| 13 |
+
padding (float): BBox padding factor that will be multilied to scale.
|
| 14 |
+
Default: 1.0
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
tuple: A tuple containing center and scale.
|
| 18 |
+
- np.ndarray[float32]: Center (x, y) of the bbox in shape (2,) or
|
| 19 |
+
(n, 2)
|
| 20 |
+
- np.ndarray[float32]: Scale (w, h) of the bbox in shape (2,) or
|
| 21 |
+
(n, 2)
|
| 22 |
+
"""
|
| 23 |
+
# convert single bbox from (4, ) to (1, 4)
|
| 24 |
+
dim = bbox.ndim
|
| 25 |
+
if dim == 1:
|
| 26 |
+
bbox = bbox[None, :]
|
| 27 |
+
|
| 28 |
+
# get bbox center and scale
|
| 29 |
+
x1, y1, x2, y2 = np.hsplit(bbox, [1, 2, 3])
|
| 30 |
+
center = np.hstack([x1 + x2, y1 + y2]) * 0.5
|
| 31 |
+
scale = np.hstack([x2 - x1, y2 - y1]) * padding
|
| 32 |
+
|
| 33 |
+
if dim == 1:
|
| 34 |
+
center = center[0]
|
| 35 |
+
scale = scale[0]
|
| 36 |
+
|
| 37 |
+
return center, scale
|
| 38 |
+
|
| 39 |
+
def decode(simcc_x: np.ndarray,
|
| 40 |
+
simcc_y: np.ndarray,
|
| 41 |
+
simcc_split_ratio,
|
| 42 |
+
use_torch=False) -> Tuple[np.ndarray, np.ndarray]:
|
| 43 |
+
"""Modulate simcc distribution with Gaussian.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
simcc_x (np.ndarray[K, Wx]): model predicted simcc in x.
|
| 47 |
+
simcc_y (np.ndarray[K, Wy]): model predicted simcc in y.
|
| 48 |
+
simcc_split_ratio (int): The split ratio of simcc.
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
tuple: A tuple containing center and scale.
|
| 52 |
+
- np.ndarray[float32]: keypoints in shape (K, 2) or (n, K, 2)
|
| 53 |
+
- np.ndarray[float32]: scores in shape (K,) or (n, K)
|
| 54 |
+
"""
|
| 55 |
+
keypoints, scores = get_simcc_maximum(simcc_x, simcc_y, use_torch=use_torch)
|
| 56 |
+
keypoints /= simcc_split_ratio
|
| 57 |
+
|
| 58 |
+
return keypoints, scores
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _fix_aspect_ratio(bbox_scale: np.ndarray,
|
| 62 |
+
aspect_ratio: float) -> np.ndarray:
|
| 63 |
+
"""Extend the scale to match the given aspect ratio.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
scale (np.ndarray): The image scale (w, h) in shape (2, )
|
| 67 |
+
aspect_ratio (float): The ratio of ``w/h``
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
np.ndarray: The reshaped image scale in (2, )
|
| 71 |
+
"""
|
| 72 |
+
w, h = np.hsplit(bbox_scale, [1])
|
| 73 |
+
bbox_scale = np.where(w > h * aspect_ratio,
|
| 74 |
+
np.hstack([w, w / aspect_ratio]),
|
| 75 |
+
np.hstack([h * aspect_ratio, h]))
|
| 76 |
+
return bbox_scale
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _rotate_point(pt: np.ndarray,
|
| 80 |
+
angle_rad: float) -> np.ndarray:
|
| 81 |
+
"""Rotate a point by an angle.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
pt (np.ndarray): 2D point coordinates (x, y) in shape (2, )
|
| 85 |
+
angle_rad (float): rotation angle in radian
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
np.ndarray: Rotated point in shape (2, )
|
| 89 |
+
"""
|
| 90 |
+
sn, cs = np.sin(angle_rad), np.cos(angle_rad)
|
| 91 |
+
rot_mat = np.array([[cs, -sn], [sn, cs]])
|
| 92 |
+
return rot_mat @ pt
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _get_3rd_point(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
| 96 |
+
"""To calculate the affine matrix, three pairs of points are required. This
|
| 97 |
+
function is used to get the 3rd point, given 2D points a & b.
|
| 98 |
+
|
| 99 |
+
The 3rd point is defined by rotating vector `a - b` by 90 degrees
|
| 100 |
+
anticlockwise, using b as the rotation center.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
a (np.ndarray): The 1st point (x,y) in shape (2, )
|
| 104 |
+
b (np.ndarray): The 2nd point (x,y) in shape (2, )
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
np.ndarray: The 3rd point.
|
| 108 |
+
"""
|
| 109 |
+
direction = a - b
|
| 110 |
+
c = b + np.r_[-direction[1], direction[0]]
|
| 111 |
+
return c
|
| 112 |
+
|
| 113 |
+
def get_warp_matrix(center: np.ndarray,
|
| 114 |
+
scale: np.ndarray,
|
| 115 |
+
rot: float,
|
| 116 |
+
output_size: Tuple[int, int],
|
| 117 |
+
shift: Tuple[float, float] = (0., 0.),
|
| 118 |
+
inv: bool = False) -> np.ndarray:
|
| 119 |
+
"""Calculate the affine transformation matrix that can warp the bbox area
|
| 120 |
+
in the input image to the output size.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
center (np.ndarray[2, ]): Center of the bounding box (x, y).
|
| 124 |
+
scale (np.ndarray[2, ]): Scale of the bounding box
|
| 125 |
+
wrt [width, height].
|
| 126 |
+
rot (float): Rotation angle (degree).
|
| 127 |
+
output_size (np.ndarray[2, ] | list(2,)): Size of the
|
| 128 |
+
destination heatmaps.
|
| 129 |
+
shift (0-100%): Shift translation ratio wrt the width/height.
|
| 130 |
+
Default (0., 0.).
|
| 131 |
+
inv (bool): Option to inverse the affine transform direction.
|
| 132 |
+
(inv=False: src->dst or inv=True: dst->src)
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
np.ndarray: A 2x3 transformation matrix
|
| 136 |
+
"""
|
| 137 |
+
shift = np.array(shift)
|
| 138 |
+
src_w = scale[0]
|
| 139 |
+
dst_w = output_size[0]
|
| 140 |
+
dst_h = output_size[1]
|
| 141 |
+
|
| 142 |
+
# compute transformation matrix
|
| 143 |
+
rot_rad = np.deg2rad(rot)
|
| 144 |
+
src_dir = _rotate_point(np.array([0., src_w * -0.5]), rot_rad)
|
| 145 |
+
dst_dir = np.array([0., dst_w * -0.5])
|
| 146 |
+
|
| 147 |
+
# get four corners of the src rectangle in the original image
|
| 148 |
+
src = np.zeros((3, 2), dtype=np.float32)
|
| 149 |
+
src[0, :] = center + scale * shift
|
| 150 |
+
src[1, :] = center + src_dir + scale * shift
|
| 151 |
+
src[2, :] = _get_3rd_point(src[0, :], src[1, :])
|
| 152 |
+
|
| 153 |
+
# get four corners of the dst rectangle in the input image
|
| 154 |
+
dst = np.zeros((3, 2), dtype=np.float32)
|
| 155 |
+
dst[0, :] = [dst_w * 0.5, dst_h * 0.5]
|
| 156 |
+
dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir
|
| 157 |
+
dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :])
|
| 158 |
+
|
| 159 |
+
if inv:
|
| 160 |
+
warp_mat = cv2.getAffineTransform(np.float32(dst), np.float32(src))
|
| 161 |
+
else:
|
| 162 |
+
warp_mat = cv2.getAffineTransform(np.float32(src), np.float32(dst))
|
| 163 |
+
|
| 164 |
+
return warp_mat
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def top_down_affine(input_size: dict,
|
| 168 |
+
bbox_scale: dict,
|
| 169 |
+
bbox_center: dict,
|
| 170 |
+
img: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
| 171 |
+
"""Get the bbox image as the model input by affine transform.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
input_size (dict): The input size of the model.
|
| 175 |
+
bbox_scale (dict): The bbox scale of the img.
|
| 176 |
+
bbox_center (dict): The bbox center of the img.
|
| 177 |
+
img (np.ndarray): The original image.
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
tuple: A tuple containing center and scale.
|
| 181 |
+
- np.ndarray[float32]: img after affine transform.
|
| 182 |
+
- np.ndarray[float32]: bbox scale after affine transform.
|
| 183 |
+
"""
|
| 184 |
+
w, h = input_size
|
| 185 |
+
warp_size = (int(w), int(h))
|
| 186 |
+
|
| 187 |
+
# reshape bbox to fixed aspect ratio
|
| 188 |
+
bbox_scale = _fix_aspect_ratio(bbox_scale, aspect_ratio=w / h)
|
| 189 |
+
|
| 190 |
+
# get the affine matrix
|
| 191 |
+
center = bbox_center
|
| 192 |
+
scale = bbox_scale
|
| 193 |
+
rot = 0
|
| 194 |
+
warp_mat = get_warp_matrix(center, scale, rot, output_size=(w, h))
|
| 195 |
+
|
| 196 |
+
# do affine transform
|
| 197 |
+
img = cv2.warpAffine(img, warp_mat, warp_size, flags=cv2.INTER_LINEAR)
|
| 198 |
+
|
| 199 |
+
return img, bbox_scale
|
| 200 |
+
|
| 201 |
+
def get_simcc_maximum(simcc_x: np.ndarray,
|
| 202 |
+
simcc_y: np.ndarray,
|
| 203 |
+
use_torch=False) -> Tuple[np.ndarray, np.ndarray]:
|
| 204 |
+
"""Get maximum response location and value from simcc representations.
|
| 205 |
+
|
| 206 |
+
Note:
|
| 207 |
+
instance number: N
|
| 208 |
+
num_keypoints: K
|
| 209 |
+
heatmap height: H
|
| 210 |
+
heatmap width: W
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
simcc_x (np.ndarray): x-axis SimCC in shape (K, Wx) or (N, K, Wx)
|
| 214 |
+
simcc_y (np.ndarray): y-axis SimCC in shape (K, Wy) or (N, K, Wy)
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
tuple:
|
| 218 |
+
- locs (np.ndarray): locations of maximum heatmap responses in shape
|
| 219 |
+
(K, 2) or (N, K, 2)
|
| 220 |
+
- vals (np.ndarray): values of maximum heatmap responses in shape
|
| 221 |
+
(K,) or (N, K)
|
| 222 |
+
"""
|
| 223 |
+
N, K, Wx = simcc_x.shape
|
| 224 |
+
simcc_x = simcc_x.reshape(N * K, -1)
|
| 225 |
+
simcc_y = simcc_y.reshape(N * K, -1)
|
| 226 |
+
|
| 227 |
+
# get maximum value locations
|
| 228 |
+
x_locs = np.argmax(simcc_x, axis=1)
|
| 229 |
+
y_locs = np.argmax(simcc_y, axis=1)
|
| 230 |
+
locs = np.stack((x_locs, y_locs), axis=-1).astype(np.float32)
|
| 231 |
+
if use_torch:
|
| 232 |
+
max_val_x = torch.max(simcc_x, dim=1)[0]
|
| 233 |
+
max_val_y = torch.max(simcc_y, dim=1)[0]
|
| 234 |
+
else:
|
| 235 |
+
max_val_x = np.amax(simcc_x, axis=1)
|
| 236 |
+
max_val_y = np.amax(simcc_y, axis=1)
|
| 237 |
+
|
| 238 |
+
# get maximum value across x and y axis
|
| 239 |
+
mask = max_val_x > max_val_y
|
| 240 |
+
max_val_x[mask] = max_val_y[mask]
|
| 241 |
+
vals = max_val_x
|
| 242 |
+
locs[vals <= 0.] = -1
|
| 243 |
+
|
| 244 |
+
# reshape
|
| 245 |
+
locs = locs.reshape(N, K, 2)
|
| 246 |
+
vals = vals.reshape(N, K)
|
| 247 |
+
|
| 248 |
+
return locs, vals
|
models/models/engine/visualizer.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import abstractmethod
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
import cv2
|
| 4 |
+
import subprocess
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
def putText(img, text: str, position,
|
| 8 |
+
text_font: int=0, text_scale: int=1,
|
| 9 |
+
bg_color=(255,255,255),
|
| 10 |
+
text_color=(255,0,255),
|
| 11 |
+
bg_thickness=8,
|
| 12 |
+
text_thickness=1,
|
| 13 |
+
lineType=cv2.LINE_AA):
|
| 14 |
+
""" Function to put text on image.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
img (_type_):
|
| 18 |
+
text (str): _description_
|
| 19 |
+
position (_type_): Top-left position of text.
|
| 20 |
+
text_font (int, optional): font size of text. Defaults to 0.
|
| 21 |
+
text_scale (int, optional): text scale. Defaults to 1.
|
| 22 |
+
bg_color (tuple, optional): text background color. Defaults to (255,255,255).
|
| 23 |
+
text_color (tuple, optional): text foreground color. Defaults to (255,0,255).
|
| 24 |
+
bg_thickness (int, optional): text background thickness. Defaults to 8.
|
| 25 |
+
text_thickness (int, optional): text foreground thickness. Defaults to 1.
|
| 26 |
+
lineType (_type_, optional): line type. Defaults to cv2.LINE_AA.
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
_type_: _description_
|
| 30 |
+
"""
|
| 31 |
+
img = cv2.putText(img, text, position, text_font, text_scale, bg_color, thickness=bg_thickness, lineType=lineType)
|
| 32 |
+
img = cv2.putText(img, text, position, text_font, text_scale, text_color, thickness=text_thickness, lineType=lineType)
|
| 33 |
+
return img
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class BaseVisualizer():
|
| 37 |
+
|
| 38 |
+
def __init__(self, class_names: Optional[List[str]], fps: int=-1, min_width: int=-1):
|
| 39 |
+
""" Visualizer class for visualization (track_results + count_results).
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
class_map_ids (Dict): class mapping dictionary to map model's class to original class. Eg {0: 1, 1: 0, 2: 2, 3: 3} mean we swap class ID between 0 and 1.
|
| 43 |
+
fps (int): FPS for output video. If fps = -1, it will have same fps as input video.
|
| 44 |
+
min_width (int): minimum width for output video (height will be scaled to keep aspect ratio as input video). If min_width = -1, it will have same resolution as input video.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
self.fps = fps
|
| 48 |
+
self.min_width = min_width
|
| 49 |
+
self.class_names = class_names
|
| 50 |
+
|
| 51 |
+
def init_writer(self, input_video_info: List[int], output_path: str):
|
| 52 |
+
""" Init video writer for write visualized frame to output video.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
input_video_info (List[int]): It is a list that includes 4 elements of input video information (fps, width, height, num_frames).
|
| 56 |
+
output_path (str): Path to save output video.
|
| 57 |
+
"""
|
| 58 |
+
if (self.fps == -1):
|
| 59 |
+
self.fps = input_video_info[0]
|
| 60 |
+
self.width, self.height = input_video_info[1], input_video_info[2]
|
| 61 |
+
if (self.min_width > 0):
|
| 62 |
+
out_width = min(self.min_width, self.width)
|
| 63 |
+
self.height = (self.height * out_width)//self.width
|
| 64 |
+
self.width = out_width
|
| 65 |
+
self.output_path = output_path
|
| 66 |
+
self.writer = cv2.VideoWriter(self.output_path, cv2.VideoWriter_fourcc(*"mp4v"), int(self.fps), (self.width, self.height))
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
def get_color(idx):
|
| 70 |
+
idx = idx * 3
|
| 71 |
+
color = ((37 * idx) % 255, (17 * idx) % 255, (29 * idx) % 255)
|
| 72 |
+
|
| 73 |
+
return color
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def draw_dash_line(img,pt1,pt2,color,thickness=1,style='dotted',gap=20):
|
| 77 |
+
dist =((pt1[0]-pt2[0])**2+(pt1[1]-pt2[1])**2)**.5
|
| 78 |
+
pts= []
|
| 79 |
+
for i in np.arange(0,dist,gap):
|
| 80 |
+
r=i/dist
|
| 81 |
+
x=int((pt1[0]*(1-r)+pt2[0]*r)+.5)
|
| 82 |
+
y=int((pt1[1]*(1-r)+pt2[1]*r)+.5)
|
| 83 |
+
p = (x,y)
|
| 84 |
+
pts.append(p)
|
| 85 |
+
if len(pts) ==0:
|
| 86 |
+
return
|
| 87 |
+
if style=='dotted':
|
| 88 |
+
for p in pts:
|
| 89 |
+
cv2.circle(img,p,thickness,color,-1)
|
| 90 |
+
else:
|
| 91 |
+
s=pts[0]
|
| 92 |
+
e=pts[0]
|
| 93 |
+
i=0
|
| 94 |
+
for p in pts:
|
| 95 |
+
s=e
|
| 96 |
+
e=p
|
| 97 |
+
if i%2==1:
|
| 98 |
+
cv2.line(img,s,e,color,thickness)
|
| 99 |
+
i+=1
|
| 100 |
+
|
| 101 |
+
@staticmethod
|
| 102 |
+
def draw_dash_poly(img,pts,color,thickness=1,style='dotted',gap=20):
|
| 103 |
+
""" draw a polygon with dash line.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
img (_type_): input image.
|
| 107 |
+
pts (_type_): _description_
|
| 108 |
+
color (_type_): _description_
|
| 109 |
+
thickness (int, optional): _description_. Defaults to 1.
|
| 110 |
+
style (str, optional): _description_. Defaults to 'dotted'.
|
| 111 |
+
gap (int, optional): _description_. Defaults to 20.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
_type_: _description_
|
| 115 |
+
"""
|
| 116 |
+
s=pts[0]
|
| 117 |
+
e=pts[0]
|
| 118 |
+
pts.append(pts.pop(0))
|
| 119 |
+
for p in pts:
|
| 120 |
+
s=e
|
| 121 |
+
e=p
|
| 122 |
+
BaseVisualizer.draw_dash_line(img,s,e,color,thickness,style,gap=gap)
|
| 123 |
+
return img
|
| 124 |
+
|
| 125 |
+
@staticmethod
|
| 126 |
+
def draw_dash_rect(img,pt1,pt2,color,thickness=1,style='dotted',gap=10):
|
| 127 |
+
pts = [pt1,(pt2[0],pt1[1]),pt2,(pt1[0],pt2[1])]
|
| 128 |
+
return BaseVisualizer.draw_dash_poly(img,pts,color,thickness,style,gap=gap)
|
| 129 |
+
|
| 130 |
+
def close(self):
|
| 131 |
+
""" Function to release video writer. It should be called after finish visualization for all input frames.
|
| 132 |
+
"""
|
| 133 |
+
self.writer.release()
|
| 134 |
+
|
| 135 |
+
def convert(self):
|
| 136 |
+
subprocess.run(f"ffmpeg -y -loglevel quiet -stats -i {self.output_path} -c:v libx264 {self.output_path}".split())
|
| 137 |
+
|
| 138 |
+
@abstractmethod
|
| 139 |
+
def visualize(self, *args,**kwargs):
|
| 140 |
+
""" Each project should implement this function to visualize a frame.
|
| 141 |
+
|
| 142 |
+
"""
|
| 143 |
+
raise NotImplementedError
|
models/models/pose/rtmpose.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) OpenMMLab. All rights reserved.
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
from typing import List, Tuple, Dict
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
from models.base.onnx_base import ONNX_Base
|
| 10 |
+
from models.base.trt_base import TRT_Base
|
| 11 |
+
from models.engine.utils import *
|
| 12 |
+
|
| 13 |
+
class RTMPose():
|
| 14 |
+
def __init__(self,
|
| 15 |
+
use_torch: bool=False) -> None:
|
| 16 |
+
self.use_torch = use_torch
|
| 17 |
+
|
| 18 |
+
def preprocess(self,
|
| 19 |
+
input_data: np.ndarray,
|
| 20 |
+
input_size: Tuple[int, int] = (192, 256)) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 21 |
+
"""Do preprocessing for RTMPose model inference.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
img (np.ndarray): Input image in shape.
|
| 25 |
+
input_size (tuple): Input image size in shape (w, h).
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
tuple:
|
| 29 |
+
- resized_img (np.ndarray): Preprocessed image.
|
| 30 |
+
- center (np.ndarray): Center of image.
|
| 31 |
+
- scale (np.ndarray): Scale of image.
|
| 32 |
+
"""
|
| 33 |
+
tensor_data = []
|
| 34 |
+
# get shape of image
|
| 35 |
+
scales =[]
|
| 36 |
+
centers = []
|
| 37 |
+
for i in range(len(input_data)):
|
| 38 |
+
img = input_data[i]
|
| 39 |
+
img_shape = img.shape[:2]
|
| 40 |
+
bbox = np.array([0, 0, img_shape[1], img_shape[0]])
|
| 41 |
+
|
| 42 |
+
# get center and scale
|
| 43 |
+
center, scale = bbox_xyxy2cs(bbox, padding=1.25)
|
| 44 |
+
# do affine transformation
|
| 45 |
+
resized_img, scale = top_down_affine(input_size, scale, center, img)
|
| 46 |
+
|
| 47 |
+
# normalize image
|
| 48 |
+
mean = np.array([123.675, 116.28, 103.53])
|
| 49 |
+
std = np.array([58.395, 57.12, 57.375])
|
| 50 |
+
resized_img = (resized_img - mean) / std
|
| 51 |
+
|
| 52 |
+
centers.append(center)
|
| 53 |
+
scales.append(scale)
|
| 54 |
+
|
| 55 |
+
if self.use_torch:
|
| 56 |
+
tensor_data.append(torch.from_numpy(resized_img).to(self.device))
|
| 57 |
+
else:
|
| 58 |
+
tensor_data.append(resized_img.transpose(2, 0, 1))
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if self.use_torch:
|
| 62 |
+
tensor_data = torch.stack(tensor_data, dim=0)[:, :, :, [2, 1, 0]].permute(0, 3, 1, 2).float().contiguous()
|
| 63 |
+
else:
|
| 64 |
+
tensor_data = np.stack(tensor_data, axis=0)
|
| 65 |
+
|
| 66 |
+
return tensor_data, centers, scales
|
| 67 |
+
|
| 68 |
+
def postprocess(self, outputs: List[np.ndarray],
|
| 69 |
+
model_input_size: Tuple[int, int],
|
| 70 |
+
centers: List[np.ndarray],
|
| 71 |
+
scales: List[np.ndarray],
|
| 72 |
+
simcc_split_ratio: float = 2.0,
|
| 73 |
+
use_torch=False
|
| 74 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 75 |
+
"""Postprocess for RTMPose model output.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
outputs (np.ndarray): Output of RTMPose model.
|
| 79 |
+
model_input_size (tuple): RTMPose model Input image size.
|
| 80 |
+
center List[tuple(int,int)]: List of Center of bbox in shape (x, y).
|
| 81 |
+
scale List[tuple(int,int)]: List of Scales of bbox in shape (w, h).
|
| 82 |
+
simcc_split_ratio (float): Split ratio of simcc.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
tuple:
|
| 86 |
+
- keypoints (np.ndarray): Rescaled keypoints.
|
| 87 |
+
- scores (np.ndarray): Model predict scores.
|
| 88 |
+
"""
|
| 89 |
+
# use simcc to decode
|
| 90 |
+
simcc_x, simcc_y = outputs
|
| 91 |
+
tensor_keypoints = []
|
| 92 |
+
tensor_scores = []
|
| 93 |
+
assert simcc_x.shape[0] == simcc_y.shape[0]
|
| 94 |
+
for i in range(simcc_x.shape[0]):
|
| 95 |
+
|
| 96 |
+
simcc_x_3d = simcc_x[i][np.newaxis, :, :]
|
| 97 |
+
simcc_y_3d = simcc_y[i][np.newaxis, :, :]
|
| 98 |
+
|
| 99 |
+
keypoints, scores = decode(simcc_x_3d, simcc_y_3d, simcc_split_ratio, use_torch=use_torch)
|
| 100 |
+
|
| 101 |
+
# rescale keypoints
|
| 102 |
+
keypoints = keypoints / model_input_size * scales[i] + centers[i] - scales[i] / 2
|
| 103 |
+
|
| 104 |
+
tensor_keypoints.append(keypoints)
|
| 105 |
+
tensor_scores.append(scores)
|
| 106 |
+
|
| 107 |
+
tensor_keypoints = np.vstack(tensor_keypoints)
|
| 108 |
+
tensor_scores = np.vstack(tensor_scores)
|
| 109 |
+
return tensor_keypoints, tensor_scores
|
| 110 |
+
|
| 111 |
+
def crop_objects(self, image: np.ndarray, bounding_boxes: np.ndarray):
|
| 112 |
+
""" Function to crop objects in input image.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
image (np.ndarray): input image with shape (H, W, C).
|
| 116 |
+
bounding_boxes (np.ndarray): Array with shape Nx4 with N is the number of objects.
|
| 117 |
+
"""
|
| 118 |
+
max_h, max_w = image.shape[:2]
|
| 119 |
+
cropped_images = []
|
| 120 |
+
for box in bounding_boxes:
|
| 121 |
+
x_top, y_top, x_bottom, y_bottom, _ = box.astype(int).tolist()
|
| 122 |
+
x_top = max(0, x_top)
|
| 123 |
+
y_top = max(0, y_top)
|
| 124 |
+
x_bottom = min(x_bottom, max_w)
|
| 125 |
+
y_bottom = min(y_bottom, max_h)
|
| 126 |
+
cropped_image = image[y_top:y_bottom, x_top:x_bottom]
|
| 127 |
+
cropped_images.append(cropped_image)
|
| 128 |
+
return cropped_images
|
| 129 |
+
class RTMPoseONNX(ONNX_Base, RTMPose):
|
| 130 |
+
def __init__(self,
|
| 131 |
+
use_torch,
|
| 132 |
+
img_shape: Tuple[int, int, int]=(3, 256, 192),
|
| 133 |
+
batch_size: int=32,
|
| 134 |
+
model_path: str="",
|
| 135 |
+
device: str='0'):
|
| 136 |
+
#/home/ccvn/Workspace/haimd/CC-Demo-Collection/end2end.onnx
|
| 137 |
+
"""_summary_
|
| 138 |
+
RTMPose ONNX class for inference, which is base on ONNX_BASE and RTMPose
|
| 139 |
+
Args:
|
| 140 |
+
use_torch (_type_): use torch tensor or numpy array in preprocess and postprocess function.
|
| 141 |
+
img_shape (Tuple[int, int], optional): _description_. Defaults to (640, 640).
|
| 142 |
+
batch_size (int, optional): _description_. Defaults to 32.
|
| 143 |
+
model_path (str, optional): _description_. Defaults to "".
|
| 144 |
+
device (str, optional): _description_. Defaults to '0'.
|
| 145 |
+
"""
|
| 146 |
+
self.img_shape = img_shape
|
| 147 |
+
self.batch_size = batch_size
|
| 148 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 149 |
+
super().__init__(input_shape, model_path, device)
|
| 150 |
+
RTMPose.__init__(self,
|
| 151 |
+
use_torch=use_torch)
|
| 152 |
+
|
| 153 |
+
def infer_batch(self, image_batch: np.ndarray):
|
| 154 |
+
|
| 155 |
+
h, w = self.session.get_inputs()[0].shape[2:]
|
| 156 |
+
model_input_size = (w, h)
|
| 157 |
+
numpy_array_data, centers, scales = self.preprocess(image_batch, model_input_size)
|
| 158 |
+
numpy_array_data = numpy_array_data.astype(np.float32)
|
| 159 |
+
results = super().infer_batch(numpy_array_data)
|
| 160 |
+
keypoints, scores = self.postprocess(results, model_input_size, centers, scales)
|
| 161 |
+
return {'keypoints':keypoints, 'scores': scores}
|
| 162 |
+
|
| 163 |
+
class RTMPoseTRT(TRT_Base, RTMPose):
|
| 164 |
+
def __init__(self,
|
| 165 |
+
use_torch,
|
| 166 |
+
img_shape: Tuple[int, int, int]=(3, 256, 192),
|
| 167 |
+
batch_size: int=1,
|
| 168 |
+
model_path: str="",
|
| 169 |
+
device: str='0',):
|
| 170 |
+
""" RTMPoseTRT class for inference, which is based on TRT_Base and RTMPose.
|
| 171 |
+
"""
|
| 172 |
+
self.img_shape = img_shape
|
| 173 |
+
self.batch_size = batch_size
|
| 174 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 175 |
+
super().__init__(input_shape, model_path, device)
|
| 176 |
+
RTMPose.__init__(self,
|
| 177 |
+
use_torch=use_torch)
|
| 178 |
+
|
| 179 |
+
def infer_batch(self, image_batch: np.ndarray):
|
| 180 |
+
|
| 181 |
+
model_input_size = (self.img_shape[-1], self.img_shape[1])
|
| 182 |
+
tensor_data, centers, scales = self.preprocess(image_batch, model_input_size)
|
| 183 |
+
self.change_runtime_dimension(input_shape=(len(tensor_data), 3, model_input_size[1], model_input_size[0]))
|
| 184 |
+
self.model['binding_addrs']['input'] = int(tensor_data.data_ptr())
|
| 185 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 186 |
+
simcc_x = self.model['bindings']['simcc_x'].data.cpu()
|
| 187 |
+
simcc_y = self.model['bindings']['simcc_y'].data.cpu()
|
| 188 |
+
|
| 189 |
+
results = (simcc_x, simcc_y)
|
| 190 |
+
keypoints, scores = self.postprocess(results, model_input_size, centers, scales, use_torch=self.use_torch)
|
| 191 |
+
return {'keypoints':keypoints, 'scores': scores}
|
| 192 |
+
|
| 193 |
+
|
models/models/reids/__init__.py
ADDED
|
File without changes
|
models/models/reids/solider.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple, Union
|
| 2 |
+
from models.base.trt_base import TRT_Base
|
| 3 |
+
from models.base.onnx_base import ONNX_Base
|
| 4 |
+
import torch
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
class SOLIDERBase():
|
| 9 |
+
def __init__(self, use_torch: bool=False):
|
| 10 |
+
""" SOLIDERBase class for inference.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
preprocess_cfg (Dict):
|
| 14 |
+
- mean (List[float, float, float]): mean offset values for preprocessing.
|
| 15 |
+
- std (List[float, float, float]): standard deviation offset values for preprocessing.
|
| 16 |
+
use_torch (bool): use torch tensor or numpy array in preprocess and postprocess function.
|
| 17 |
+
"""
|
| 18 |
+
self.use_torch = use_torch
|
| 19 |
+
|
| 20 |
+
def crop_objects(self, image: np.ndarray, bounding_boxes: np.ndarray):
|
| 21 |
+
""" Function to crop objects in input image.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
image (np.ndarray): input image with shape (H, W, C).
|
| 25 |
+
bounding_boxes (np.ndarray): Array with shape Nx4 with N is the number of objects.
|
| 26 |
+
"""
|
| 27 |
+
max_h, max_w = image.shape[:2]
|
| 28 |
+
cropped_images = []
|
| 29 |
+
for box in bounding_boxes:
|
| 30 |
+
x_top, y_top, x_bottom, y_bottom, _ = box.astype(int).tolist()
|
| 31 |
+
x_top = max(0, x_top)
|
| 32 |
+
y_top = max(0, y_top)
|
| 33 |
+
x_bottom = min(x_bottom, max_w)
|
| 34 |
+
y_bottom = min(y_bottom, max_h)
|
| 35 |
+
cropped_image = image[y_top:y_bottom, x_top:x_bottom]
|
| 36 |
+
cropped_images.append(cropped_image)
|
| 37 |
+
return cropped_images
|
| 38 |
+
|
| 39 |
+
def preprocess(self, input_data: np.ndarray):
|
| 40 |
+
""" Preprocess function for input data.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
input_data (np.ndarray): batch input image.
|
| 44 |
+
"""
|
| 45 |
+
tensor_data = []
|
| 46 |
+
if ((isinstance(input_data, np.ndarray)) and (len(input_data.shape) == 3)):
|
| 47 |
+
input_data = [input_data]
|
| 48 |
+
for i in range(len(input_data)):
|
| 49 |
+
img = input_data[i]
|
| 50 |
+
img = cv2.resize(img, self.input_shape[2:][::-1], interpolation=cv2.INTER_LINEAR)
|
| 51 |
+
if self.use_torch:
|
| 52 |
+
tensor_data.append(torch.from_numpy(img).to(self.device))
|
| 53 |
+
else:
|
| 54 |
+
tensor_data.append(img)
|
| 55 |
+
if self.use_torch:
|
| 56 |
+
tensor_data = torch.stack(tensor_data, dim=0)
|
| 57 |
+
tensor_data = tensor_data.permute(0, 3, 1, 2).float().contiguous()
|
| 58 |
+
else:
|
| 59 |
+
tensor_data = np.stack(tensor_data, axis=0)
|
| 60 |
+
tensor_data = tensor_data.transpose((0, 3, 1, 2)).astype(np.float32)
|
| 61 |
+
return tensor_data
|
| 62 |
+
|
| 63 |
+
def postprocess(self, embeddings: Union[torch.tensor,np.ndarray]):
|
| 64 |
+
""" Postprocess function for input data.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
embeddings (torch.tensor or np.ndarray): output embeddingss.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
if (self.use_torch):
|
| 71 |
+
norms = torch.unsqueeze(torch.norm(embeddings, dim=-1), dim=-1)
|
| 72 |
+
else:
|
| 73 |
+
norms = np.expand_dims(np.linalg.norm(embeddings, axis=-1), axis=-1)
|
| 74 |
+
|
| 75 |
+
normalized_embeddings = embeddings/norms
|
| 76 |
+
return normalized_embeddings
|
| 77 |
+
|
| 78 |
+
def batch_padding(self, input_data: Union[torch.tensor, np.ndarray], batch_size: int) -> Union[torch.tensor, np.ndarray]:
|
| 79 |
+
"""Since the current model does not support Dynamic batch size, we perform padding to the input data.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
input_data (Union[torch.tensor, np.ndarray]): input data.
|
| 83 |
+
batch_size (int): batch size for inference.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
input_data (Union[torch.tensor, np.ndarray]): input data.
|
| 87 |
+
"""
|
| 88 |
+
n_pad = batch_size - len(input_data)
|
| 89 |
+
if n_pad>0:
|
| 90 |
+
if self.use_torch:
|
| 91 |
+
input_data = torch.cat((input_data, input_data[:n_pad]), dim=0)
|
| 92 |
+
else:
|
| 93 |
+
input_data = np.concatenate((input_data, input_data[:n_pad]), axis=0)
|
| 94 |
+
return input_data
|
| 95 |
+
|
| 96 |
+
class SOLIDERONNX(ONNX_Base, SOLIDERBase):
|
| 97 |
+
def __init__(self,
|
| 98 |
+
batch_size: int,
|
| 99 |
+
model_path: str,
|
| 100 |
+
img_shape: Tuple[int, int, int]=(3, 384, 128),
|
| 101 |
+
device: str='0',):
|
| 102 |
+
""" SOLIDER ONNX class for inference, which is based on ONNX_Base and SOLIDERBase.
|
| 103 |
+
"""
|
| 104 |
+
self.img_shape = img_shape
|
| 105 |
+
self.batch_size = batch_size
|
| 106 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 107 |
+
super().__init__(input_shape, model_path, device)
|
| 108 |
+
SOLIDERBase.__init__(self, use_torch=False)
|
| 109 |
+
|
| 110 |
+
def infer_batch(self, image_batch: np.ndarray) -> np.ndarray:
|
| 111 |
+
""" Batch inference function for batch input image.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
image_batch (np.ndarray): batch of input image.
|
| 115 |
+
"""
|
| 116 |
+
num_images = len(image_batch)
|
| 117 |
+
assert num_images <= self.batch_size, "the number of input images must be smaller or equal to the Batch size."
|
| 118 |
+
numpy_array_data = self.preprocess(image_batch)
|
| 119 |
+
|
| 120 |
+
# Padding data to the batch size
|
| 121 |
+
padding_data = self.batch_padding(numpy_array_data, self.batch_size)
|
| 122 |
+
results = super().infer_batch(padding_data)
|
| 123 |
+
|
| 124 |
+
# Crop the padding data and postprocess
|
| 125 |
+
feats = results[0][:num_images]
|
| 126 |
+
feats = self.postprocess(feats)
|
| 127 |
+
return feats
|
| 128 |
+
|
| 129 |
+
class SOLIDERTRT(TRT_Base, SOLIDERBase):
|
| 130 |
+
def __init__(self,
|
| 131 |
+
batch_size: int,
|
| 132 |
+
model_path: str,
|
| 133 |
+
img_shape: Tuple[int, int, int]=(3, 384, 128),
|
| 134 |
+
device: str='0',):
|
| 135 |
+
""" SOLIDER TRT class for inference, which is based on TRT_Base and SOLIDERBase.
|
| 136 |
+
"""
|
| 137 |
+
self.img_shape = img_shape
|
| 138 |
+
self.batch_size = batch_size
|
| 139 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 140 |
+
super().__init__(input_shape, model_path, device)
|
| 141 |
+
SOLIDERBase.__init__(self, use_torch=True)
|
| 142 |
+
|
| 143 |
+
def infer_batch(self, image_batch: np.ndarray) -> np.ndarray:
|
| 144 |
+
""" Batch inference function for batch input image.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
image_batch (np.ndarray): batch of input image.
|
| 148 |
+
"""
|
| 149 |
+
num_images = len(image_batch)
|
| 150 |
+
assert num_images <= self.batch_size, "the number of input images must be smaller or equal to the Batch size."
|
| 151 |
+
tensor_data = self.preprocess(image_batch)
|
| 152 |
+
|
| 153 |
+
# Padding data to the batch size
|
| 154 |
+
padding_data = self.batch_padding(tensor_data, self.batch_size)
|
| 155 |
+
self.model['binding_addrs']['input'] = int(padding_data.data_ptr())
|
| 156 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 157 |
+
feats = self.model['bindings']['output'].data.cpu()
|
| 158 |
+
|
| 159 |
+
# Crop the padding data and postprocess
|
| 160 |
+
feats = feats[:num_images]
|
| 161 |
+
feats = self.postprocess(feats)
|
| 162 |
+
feats = feats.float().numpy()
|
| 163 |
+
return feats
|
| 164 |
+
|
| 165 |
+
|
models/models/trackers/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .byte_track import BYTETracker
|
| 2 |
+
from .reid_parallel_tracker import ParallelTracker
|
models/models/trackers/byte_track.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mmdet.models.trackers.byte_tracker import ByteTracker as MMByteTracker
|
| 2 |
+
from mmdet.structures import DetDataSample
|
| 3 |
+
from mmengine.structures import InstanceData
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
class BYTETracker(MMByteTracker):
|
| 9 |
+
def __init__(self, *args, **kwargs):
|
| 10 |
+
""" ByteTracker class for tracking.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
obj_score_thrs (Dict):
|
| 14 |
+
- high (float): if detection box > high -> high_score_detections for first association.
|
| 15 |
+
- low (float): if low < detection box < high -> low_score_detections for second association.
|
| 16 |
+
init_track_thr (float): Detection score threshold for initializing a new tracklet.
|
| 17 |
+
weight_iou_with_det_scores (bool): Whether using detection scores to weight IOU which is used for matching.
|
| 18 |
+
match_iou_thrs (Dict): IOU distance threshold for matching between two frames.
|
| 19 |
+
- high (float): Threshold of the first matching.
|
| 20 |
+
- low (float): Threshold of the second matching.
|
| 21 |
+
- tentative (float): Threshold of the matching for tentative tracklets.
|
| 22 |
+
num_frames_retain (int): If a track is disappeared more than num_frames_retain frames, it will be deleted in the memo.
|
| 23 |
+
motion (Dict): Config for motion.
|
| 24 |
+
- type (str): Motion type (KalmanFilter, LinearFilter).
|
| 25 |
+
"""
|
| 26 |
+
super().__init__(*args, **kwargs)
|
| 27 |
+
|
| 28 |
+
def prepare_det_data_sample(self, frame_idx: int, boxes: torch.tensor, labels: torch.tensor, scores: torch.tensor):
|
| 29 |
+
""" Function to prepare DetDataSample.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
frame_idx (int): frame_idx of this frame.
|
| 33 |
+
boxes (torch.tensor(N, 4)): bounding boxes prediction of this frame.
|
| 34 |
+
labels (torch.tensor(N)): labels prediction of this frame.
|
| 35 |
+
scores (torch.tensor(N)): scores prediction of this frame.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
det_data_sample = DetDataSample()
|
| 39 |
+
det_data_sample.meta_info = dict(frame_id=frame_idx)
|
| 40 |
+
det_data_sample.pred_instances = InstanceData()
|
| 41 |
+
det_data_sample.pred_instances.bboxes = boxes
|
| 42 |
+
det_data_sample.pred_instances.labels = labels
|
| 43 |
+
det_data_sample.pred_instances.scores = scores
|
| 44 |
+
return det_data_sample
|
| 45 |
+
|
| 46 |
+
def track_batch(self, start_frame_idx: int, det_results: List[Dict], conf_thres: float=0.0):
|
| 47 |
+
""" Batch inference function for batch det results.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
start_frame_idx (int): start_frame_idx of this batch.
|
| 51 |
+
det_results (List[Dict]): detection results of this batch.
|
| 52 |
+
conf_thres (float): If a tracklet's confidence < confidence threshold, it will be removed.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
track_outputs = []
|
| 56 |
+
for frame_id, frame_det_outputs in enumerate(det_results):
|
| 57 |
+
boxes, labels = frame_det_outputs.pop("boxes"), frame_det_outputs.pop("labels")
|
| 58 |
+
det_data_sample = self.prepare_det_data_sample(frame_id+start_frame_idx, boxes[:, :4], labels, boxes[:, 4])
|
| 59 |
+
track_instances = self.track(det_data_sample)
|
| 60 |
+
boxes = torch.cat([track_instances.bboxes, torch.unsqueeze(track_instances.scores, dim=-1)], dim=-1)
|
| 61 |
+
labels = track_instances.labels
|
| 62 |
+
track_ids = track_instances.instances_id
|
| 63 |
+
boxes = np.around(boxes.numpy(),decimals=3)
|
| 64 |
+
labels = labels.numpy().astype(np.uint)
|
| 65 |
+
track_ids = track_ids.numpy().astype(np.uint)
|
| 66 |
+
idxs = np.where(boxes[:, 4] > conf_thres)[0]
|
| 67 |
+
|
| 68 |
+
track_outputs.append({
|
| 69 |
+
"boxes": boxes[idxs],
|
| 70 |
+
"labels": labels[idxs],
|
| 71 |
+
"ids": track_ids[idxs]
|
| 72 |
+
})
|
| 73 |
+
return track_outputs
|
| 74 |
+
|
models/models/trackers/reid_parallel_tracker/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .parallel_tracker import ParallelTracker
|
models/models/trackers/reid_parallel_tracker/base_tracker.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from typing import List, Tuple, Dict
|
| 3 |
+
import numpy as np
|
| 4 |
+
from .core.tracklet import (Tracklet, TrackState, add_stracks, subtract_stracks, remove_duplicate_stracks)
|
| 5 |
+
from .core.kalman_filter import KalmanFilter
|
| 6 |
+
from .core.basetrack import BaseTrack
|
| 7 |
+
from .core.matching import iou_scores
|
| 8 |
+
|
| 9 |
+
class BaseTracker(object):
|
| 10 |
+
def __init__(self,
|
| 11 |
+
det_thr=dict(high=0.3,low=0.1, min_height=10, min_width=10),
|
| 12 |
+
new_track_cfg = dict(active_thr=0.9, active_iou=0.7, thr=0.4, min_size=(10,5), feat_buffer=30),
|
| 13 |
+
lost_track_cfg = dict(max_length=32, min_size=(10,5)),
|
| 14 |
+
smooth_update = False,
|
| 15 |
+
):
|
| 16 |
+
""" Base class for SORT tracker
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
det_thr (dict, optional):
|
| 20 |
+
+ high: threshold score to consider highly confident detection.
|
| 21 |
+
Defaults to 0.3.
|
| 22 |
+
+ low : threshold score to consider low confident detection.
|
| 23 |
+
Detection with lower score than this threshold are ignored.
|
| 24 |
+
Defaults to 0.1.
|
| 25 |
+
|
| 26 |
+
new_track_cfg (dict, optional): Config for initializing new track.
|
| 27 |
+
+ thr (float, optional): threshold to initialize new track.
|
| 28 |
+
A detection with score higher than this threshold will be initialized as a new (unconfirmed) track if it does not match with any tracks.
|
| 29 |
+
Defaults to 0.4.
|
| 30 |
+
+ active_thr (float, optional): threshold to activate a new track.
|
| 31 |
+
+ active_iou (float, optional): threshold to activate a new track. A new track (score > active)thr and iou < active_iou) is a high confident detection without significant overlap with other objects are activated immediatly without confirming in the next frames.
|
| 32 |
+
+ min_size (tuple, optional): minimum (high,width) of the bounding box to be considered as new track.
|
| 33 |
+
Defaults to (80,40).
|
| 34 |
+
+ feat_buffer (int, optional): number of frames to store the features of the new track.
|
| 35 |
+
lost_track_cfg (dict, optional): Config for lost track.
|
| 36 |
+
+ max_length (int): number of frames that the lost tracks are keep before being removed. It is also the length of buffer to store the features.
|
| 37 |
+
Defaults to 30.
|
| 38 |
+
+ min_size (tuple, optional): If the lost object size smaller than this min_size(high,width) will be removed.
|
| 39 |
+
Defaults to (40,20).
|
| 40 |
+
+ tracking_region (x1,y1,x2,y2): Top-Left, Bottom-right coordinates of the tracking region. If objects move out of this region, they will be removed.
|
| 41 |
+
smooth_update (bool, optional): If True, when a lost object is refind, we interpolate its missing coordinate during lost, and use these interpolated bboxes to update Kalman Filter. Thus, avoid excessive gain when updating the Kalman filter (smoother).
|
| 42 |
+
"""
|
| 43 |
+
self.tracked_stracks = [] # type: list[Tracklet]
|
| 44 |
+
self.lost_stracks = [] # type: list[Tracklet]
|
| 45 |
+
self.removed_stracks = [] # type: list[Tracklet]
|
| 46 |
+
BaseTrack.clear_count()
|
| 47 |
+
|
| 48 |
+
self.frame_id = 0
|
| 49 |
+
self.det_thr = det_thr
|
| 50 |
+
self.new_track_cfg = new_track_cfg
|
| 51 |
+
self.lost_track_cfg = lost_track_cfg
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
self.kalman_filter = KalmanFilter()
|
| 55 |
+
self.smooth_update = smooth_update
|
| 56 |
+
|
| 57 |
+
def preprocess_det_result(self,det_results: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
| 58 |
+
boxes = det_results['boxes']
|
| 59 |
+
boxes = boxes.reshape(-1, 5)
|
| 60 |
+
h = boxes[:,3]-boxes[:,1]
|
| 61 |
+
w = boxes[:,2]-boxes[:,0]
|
| 62 |
+
valid_inds = np.logical_and(h>self.det_thr["min_height"], w>self.det_thr["min_width"])
|
| 63 |
+
for k,v in det_results.items():
|
| 64 |
+
if k in ['boxes', 'labels', 'angles', 'obj_imgs', 'embeddings']:
|
| 65 |
+
if k == 'obj_imgs':
|
| 66 |
+
det_results[k] = [v[_i] for _i, _valid in enumerate(valid_inds) if _valid]
|
| 67 |
+
else:
|
| 68 |
+
det_results[k] = v[valid_inds]
|
| 69 |
+
return det_results
|
| 70 |
+
|
| 71 |
+
def split_detections_by_scores(self,
|
| 72 |
+
det_result: Dict[str, np.ndarray])-> Tuple[List[Tracklet], List[Tracklet]]:
|
| 73 |
+
""" Split the detections into high score/lower score group.
|
| 74 |
+
det_result is a dict of {'boxes': np.ndarray(x1,y1,x2,y2,score), 'labels': np.ndarray}
|
| 75 |
+
Return:
|
| 76 |
+
detections_high: list[Tracklet]
|
| 77 |
+
detections_low: list[Tracklet]
|
| 78 |
+
"""
|
| 79 |
+
detections_high = []
|
| 80 |
+
detections_low = []
|
| 81 |
+
|
| 82 |
+
feat_history = self.new_track_cfg["feat_buffer"]
|
| 83 |
+
if len(det_result['boxes']):
|
| 84 |
+
bboxes = det_result['boxes'][:, :4]
|
| 85 |
+
scores = det_result['boxes'][:, 4]
|
| 86 |
+
classes = det_result['labels']
|
| 87 |
+
angles = np.array(det_result.get('angles',[None]*len(scores)))
|
| 88 |
+
features = np.array(det_result.get('embeddings',[None]*len(scores)))
|
| 89 |
+
obj_imgs = np.array(det_result.get('obj_imgs',[None]*len(scores)))
|
| 90 |
+
|
| 91 |
+
# Find high threshold detections
|
| 92 |
+
inds_high = scores >= self.det_thr["high"]
|
| 93 |
+
|
| 94 |
+
enable_reid_buffer = False
|
| 95 |
+
if hasattr(self, 'enable_reid_buffer'):
|
| 96 |
+
enable_reid_buffer = self.enable_reid_buffer
|
| 97 |
+
|
| 98 |
+
if np.any(inds_high):
|
| 99 |
+
detections_high = [Tracklet(Tracklet.tlbr_to_tlwh(tlbr), s, c, a, feat,feat_history=feat_history,
|
| 100 |
+
obj_img=obj_img, enable_buffer=enable_reid_buffer) for
|
| 101 |
+
(tlbr, s, c, a ,feat, obj_img) in zip(bboxes[inds_high], scores[inds_high], classes[inds_high],
|
| 102 |
+
angles[inds_high], features[inds_high], obj_imgs[inds_high])]
|
| 103 |
+
# Find low threshold detections
|
| 104 |
+
inds_low = np.logical_and(scores > self.det_thr["low"],
|
| 105 |
+
scores < self.det_thr["high"])
|
| 106 |
+
if np.any(inds_low):
|
| 107 |
+
detections_low = [Tracklet(Tracklet.tlbr_to_tlwh(tlbr), s, c, a, feat, feat_history=feat_history,
|
| 108 |
+
obj_img=obj_img, enable_buffer=enable_reid_buffer) for
|
| 109 |
+
(tlbr, s, c, a, feat, obj_img) in zip(bboxes[inds_low], scores[inds_low], classes[inds_low],
|
| 110 |
+
angles[inds_low], features[inds_low], obj_imgs[inds_low])]
|
| 111 |
+
|
| 112 |
+
return detections_high, detections_low
|
| 113 |
+
|
| 114 |
+
def split_tracks_by_activation(self) -> Tuple[List[Tracklet], List[Tracklet]]:
|
| 115 |
+
""" Split the tracks into trackpool=(tracked_tracks + lost_tracks) and unconfirmed (just initialize)
|
| 116 |
+
Returns:
|
| 117 |
+
strack_pool: List[Tracklet]
|
| 118 |
+
unconfirmed: List[Tracklet]
|
| 119 |
+
"""
|
| 120 |
+
unconfirmed = []
|
| 121 |
+
tracked_stracks = [] # type: list[Tracklet]
|
| 122 |
+
for track in self.tracked_stracks:
|
| 123 |
+
if not track.is_activated:
|
| 124 |
+
unconfirmed.append(track)
|
| 125 |
+
else:
|
| 126 |
+
tracked_stracks.append(track)
|
| 127 |
+
strack_pool = add_stracks(tracked_stracks, self.lost_stracks)
|
| 128 |
+
return strack_pool, unconfirmed
|
| 129 |
+
|
| 130 |
+
def predict_with_gmc(self,
|
| 131 |
+
strack_pool: List[Tracklet],
|
| 132 |
+
unconfirmed: List[Tracklet],
|
| 133 |
+
Hmat: np.array=None) -> None:
|
| 134 |
+
""" Predict the current location with KF, and compensate for Camera Motion
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
strack_pool (List[Tracklet]): list of tracked tracks
|
| 138 |
+
unconfirmed (List[Tracklet]): list of unconfirmed tracks
|
| 139 |
+
Hmat (np.array): Homography transformation matrix
|
| 140 |
+
"""
|
| 141 |
+
Tracklet.multi_predict(strack_pool)
|
| 142 |
+
if Hmat is not None:
|
| 143 |
+
Tracklet.multi_gmc(strack_pool,Hmat)
|
| 144 |
+
Tracklet.multi_gmc(unconfirmed,Hmat)
|
| 145 |
+
|
| 146 |
+
def update_matched_tracks(self,
|
| 147 |
+
matches: np.ndarray,
|
| 148 |
+
strack_pool: List[Tracklet],
|
| 149 |
+
detections: List[Tracklet])-> Tuple[List[Tracklet], List[Tracklet]]:
|
| 150 |
+
"""Update the matched tracks with Kalman Filter
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
matches (np.ndarray): [Nx2] index of the matched tracks and detections
|
| 154 |
+
strack_pool (List[Tracklet]): List of tracked tracks
|
| 155 |
+
detections (List[Tracklet]): List of detections
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
activated_stracks (List[Tracklet]): List of tracked tracks that continue to be tracked (activated)
|
| 159 |
+
refind_stracks (List[Tracklet]): List of lost tracks that are refound in this frame (refind)
|
| 160 |
+
"""
|
| 161 |
+
activated_stracks,refind_stracks=[],[]
|
| 162 |
+
for itracked, idet in matches:
|
| 163 |
+
track = strack_pool[itracked]
|
| 164 |
+
det = detections[idet]
|
| 165 |
+
if track.state == TrackState.Tracked:
|
| 166 |
+
#Perform Kalman Update/Feature Update
|
| 167 |
+
if self.smooth_update:
|
| 168 |
+
track.smooth_update(det, self.frame_id)
|
| 169 |
+
else:
|
| 170 |
+
track.update(det, self.frame_id)
|
| 171 |
+
activated_stracks.append(track)
|
| 172 |
+
else:
|
| 173 |
+
track.re_activate(det, self.frame_id, new_id=False)
|
| 174 |
+
refind_stracks.append(track)
|
| 175 |
+
|
| 176 |
+
return activated_stracks,refind_stracks
|
| 177 |
+
|
| 178 |
+
def init_new_tracks(self,
|
| 179 |
+
detections: List[Tracklet],
|
| 180 |
+
u_detection: np.ndarray)-> List[Tracklet]:
|
| 181 |
+
"""Initialize new tracks
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
detections (List[Tracklet]): List of detection objects
|
| 185 |
+
u_detection (np.ndarray): indices of the detections that are not matched with any tracks,
|
| 186 |
+
and are considerd as new detections if its score is high enough
|
| 187 |
+
Returns:
|
| 188 |
+
List[Tracklet]: List of new tracks
|
| 189 |
+
"""
|
| 190 |
+
new_tracks= []
|
| 191 |
+
for inew in u_detection:
|
| 192 |
+
det_= detections[inew]
|
| 193 |
+
if det_.score >=self.new_track_cfg["thr"] and (not det_.is_too_small(self.new_track_cfg["min_size"])):
|
| 194 |
+
new_tracks.append(det_)
|
| 195 |
+
# The activate function will initialize the new tracks with a new id
|
| 196 |
+
for track in new_tracks:
|
| 197 |
+
# By default, new_track status is Unconfirmed (is_activated = False), except for the first frame
|
| 198 |
+
track.activate(self.kalman_filter, self.frame_id)
|
| 199 |
+
return new_tracks
|
| 200 |
+
|
| 201 |
+
def activate_new_tracks(self,new_tracks, current_tracks):
|
| 202 |
+
ious =iou_scores(new_tracks, current_tracks)
|
| 203 |
+
iou_max = ious.max(axis=1) if ious.shape[1]>0 else np.zeros(len(new_tracks))
|
| 204 |
+
active_thr = self.new_track_cfg.get('active_thr',0.7)
|
| 205 |
+
active_iou = self.new_track_cfg.get('active_iou',0.5)
|
| 206 |
+
for track, iou in zip(new_tracks, iou_max):
|
| 207 |
+
# For very high confident detection and non-overlap objects, we can activate it directly
|
| 208 |
+
if track.score >= active_thr and iou < active_iou:
|
| 209 |
+
track.mark_activated()
|
| 210 |
+
|
| 211 |
+
def remove_lost_tracks(self):
|
| 212 |
+
removed_stracks=[]
|
| 213 |
+
""" Remove lost tracks if they are already lost for a certain conditions"""
|
| 214 |
+
for track in self.lost_stracks:
|
| 215 |
+
is_expired = track.is_expired(self.frame_id, self.lost_track_cfg["max_length"])
|
| 216 |
+
is_out_border, is_too_small = False, False
|
| 217 |
+
if self.lost_track_cfg.get('tracking_region',None) is not None:
|
| 218 |
+
is_out_border = track.is_out_border(self.lost_track_cfg["tracking_region"])
|
| 219 |
+
if self.lost_track_cfg.get('min_size',None) is not None:
|
| 220 |
+
is_too_small = track.is_too_small(self.lost_track_cfg["min_size"])
|
| 221 |
+
|
| 222 |
+
if is_expired or is_out_border or is_too_small:
|
| 223 |
+
track.mark_removed()
|
| 224 |
+
removed_stracks.append(track)
|
| 225 |
+
return removed_stracks
|
| 226 |
+
|
| 227 |
+
def merge_results(self,
|
| 228 |
+
activated_stracks: List[Tracklet],
|
| 229 |
+
refind_stracks: List[Tracklet],
|
| 230 |
+
new_stracks: List[Tracklet],
|
| 231 |
+
removed_stracks: List[Tracklet]) -> Tuple[Dict, Dict]:
|
| 232 |
+
""" Merge the results from different types of tracks into the final results
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
activated_stracks (List[Tracklet]): activated tracks
|
| 236 |
+
refind_stracks (List[Tracklet]): refind tracks
|
| 237 |
+
removed_stracks (List[Tracklet]): removed tracks
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
active_tracks (dict): dict of active tracks in the current frame. See format_track_results for the format.
|
| 241 |
+
lost_tracks (dict): dict of lost tracks in the current frame. See format_track_results for the format.
|
| 242 |
+
"""
|
| 243 |
+
self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
|
| 244 |
+
self.tracked_stracks = add_stracks(self.tracked_stracks, activated_stracks)
|
| 245 |
+
self.tracked_stracks = add_stracks(self.tracked_stracks, refind_stracks)
|
| 246 |
+
self.tracked_stracks = add_stracks(self.tracked_stracks, new_stracks)
|
| 247 |
+
self.lost_stracks = subtract_stracks(self.lost_stracks, self.tracked_stracks)
|
| 248 |
+
self.lost_stracks = subtract_stracks(self.lost_stracks, self.removed_stracks)
|
| 249 |
+
self.removed_stracks.extend(removed_stracks)
|
| 250 |
+
self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
|
| 251 |
+
|
| 252 |
+
active_tracks = [track for track in self.tracked_stracks if track.is_activated]
|
| 253 |
+
active_tracks = self.format_track_results(active_tracks) if len(active_tracks)>0 else None
|
| 254 |
+
lost_tracks = self.format_track_results(self.lost_stracks) if len(self.lost_stracks)>0 else None
|
| 255 |
+
return active_tracks, lost_tracks
|
| 256 |
+
|
| 257 |
+
def format_track_results(self,
|
| 258 |
+
tracklets: List[Tracklet]) -> Dict[str, np.ndarray]:
|
| 259 |
+
"""Format the tracking results to the required format
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
tracklets (Lost): _description_
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
_type_: _description_
|
| 266 |
+
"""
|
| 267 |
+
tlbrs = []
|
| 268 |
+
ids = []
|
| 269 |
+
scores = []
|
| 270 |
+
cls = []
|
| 271 |
+
vel = []
|
| 272 |
+
angles = []
|
| 273 |
+
for t in tracklets:
|
| 274 |
+
tlbrs.append(t.tlbr)
|
| 275 |
+
ids.append(t.track_id)
|
| 276 |
+
scores.append(t.score)
|
| 277 |
+
cls.append(t.cls)
|
| 278 |
+
vel.append(t.vel_dir)
|
| 279 |
+
angles.append(t.angle)
|
| 280 |
+
|
| 281 |
+
track_outputs={
|
| 282 |
+
"boxes": np.concatenate([np.array(tlbrs), np.expand_dims(np.array(scores), axis=1)], axis=1),
|
| 283 |
+
"labels": np.array(cls),
|
| 284 |
+
"ids": np.array(ids),
|
| 285 |
+
"velocity": np.array(vel), # motion velocity
|
| 286 |
+
"angles": np.array(angles), # body orientation
|
| 287 |
+
}
|
| 288 |
+
return track_outputs
|
| 289 |
+
|
| 290 |
+
def update(self,
|
| 291 |
+
det_result: Dict,
|
| 292 |
+
Hmat: np.array=None,
|
| 293 |
+
meta_data: Dict=None) -> Tuple[Dict, Dict]:
|
| 294 |
+
""" The main function to perform tracking, which may includes the follow steps:
|
| 295 |
+
1. Split the detections into high score/lower score group:
|
| 296 |
+
- split_detections_by_scores
|
| 297 |
+
2. Split the tracks into trackpool=(tracked_tracks + lost_tracks) and unconfirmed (just initialize).
|
| 298 |
+
- split_tracks_by_activation
|
| 299 |
+
- predict_with_gmc: predict the current location of these tracklets with KF, and compensate for Camera Motion
|
| 300 |
+
3. First association with high score detection boxes:
|
| 301 |
+
- matcher_high
|
| 302 |
+
- update_matched_tracks
|
| 303 |
+
4. Second association with low score detection boxes
|
| 304 |
+
- matcher_low
|
| 305 |
+
- update_matched_tracks if they are activated or refind
|
| 306 |
+
- mark new lost tracks
|
| 307 |
+
5. Third association, between new detections and unconfirmed tracks (usually tracks with only one beginning frame)
|
| 308 |
+
- matcher_unconfirmed
|
| 309 |
+
- remove unconfirmed tracks that does not match any detections
|
| 310 |
+
- init new track if the unconfirmed track is matched with a detection
|
| 311 |
+
6. Remove lost tracks if they are already lost for a certain frames
|
| 312 |
+
7. Update status for these trackes: active, lost, removed, uncofirmed.
|
| 313 |
+
Merge results and format the results
|
| 314 |
+
"""
|
| 315 |
+
raise NotImplementedError
|
models/models/trackers/reid_parallel_tracker/core/__init__.py
ADDED
|
File without changes
|
models/models/trackers/reid_parallel_tracker/core/basetrack.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from collections import OrderedDict
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class TrackState(object):
|
| 6 |
+
New = 0
|
| 7 |
+
Tracked = 1
|
| 8 |
+
Lost = 2
|
| 9 |
+
LongLost = 3
|
| 10 |
+
Removed = 4
|
| 11 |
+
Merged = 5
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BaseTrack(object):
|
| 15 |
+
_count = 0
|
| 16 |
+
|
| 17 |
+
track_id = 0
|
| 18 |
+
is_activated = False
|
| 19 |
+
state = TrackState.New
|
| 20 |
+
|
| 21 |
+
history = OrderedDict()
|
| 22 |
+
feat_buffer = []
|
| 23 |
+
curr_feature = None
|
| 24 |
+
score = 0
|
| 25 |
+
start_frame = 0
|
| 26 |
+
frame_id = 0
|
| 27 |
+
time_since_update = 0
|
| 28 |
+
|
| 29 |
+
# multi-camera
|
| 30 |
+
location = (np.inf, np.inf)
|
| 31 |
+
|
| 32 |
+
is_merge = False
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def end_frame(self):
|
| 36 |
+
return self.frame_id
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def next_id():
|
| 40 |
+
BaseTrack._count += 1
|
| 41 |
+
return BaseTrack._count
|
| 42 |
+
|
| 43 |
+
def activate(self, *args):
|
| 44 |
+
raise NotImplementedError
|
| 45 |
+
|
| 46 |
+
def predict(self):
|
| 47 |
+
raise NotImplementedError
|
| 48 |
+
|
| 49 |
+
def update(self, *args, **kwargs):
|
| 50 |
+
raise NotImplementedError
|
| 51 |
+
|
| 52 |
+
def mark_lost(self):
|
| 53 |
+
self.state = TrackState.Lost
|
| 54 |
+
|
| 55 |
+
def mark_long_lost(self):
|
| 56 |
+
self.state = TrackState.LongLost
|
| 57 |
+
|
| 58 |
+
def mark_removed(self):
|
| 59 |
+
self.state = TrackState.Removed
|
| 60 |
+
|
| 61 |
+
def mark_merged(self):
|
| 62 |
+
self.state = TrackState.Merged
|
| 63 |
+
|
| 64 |
+
def mark_activated(self):
|
| 65 |
+
self.is_activated = True
|
| 66 |
+
|
| 67 |
+
def is_expired(self, frame_id, max_time_lost):
|
| 68 |
+
is_expired = (frame_id - self.end_frame) >= max_time_lost
|
| 69 |
+
return is_expired
|
| 70 |
+
|
| 71 |
+
def is_out_border(self, tracking_region):
|
| 72 |
+
tlbr = self.tlbr
|
| 73 |
+
is_out= (tlbr[0] <= tracking_region[0]) or (tlbr[1] <= tracking_region[1]) or \
|
| 74 |
+
(tlbr[2] >= tracking_region[2]) or (tlbr[3] >= tracking_region[3])
|
| 75 |
+
return is_out
|
| 76 |
+
|
| 77 |
+
def is_too_small(self, min_size):
|
| 78 |
+
min_h, min_w = min_size
|
| 79 |
+
x,y,w,h = tuple(self.xywh)
|
| 80 |
+
return (w < min_w) or (h < min_h) or (w*h < min_h*min_w)
|
| 81 |
+
|
| 82 |
+
def is_active(self):
|
| 83 |
+
return self.state == TrackState.Tracked or self.state == TrackState.New
|
| 84 |
+
|
| 85 |
+
def is_lost(self):
|
| 86 |
+
return self.state == TrackState.Lost or self.state == TrackState.LongLost
|
| 87 |
+
|
| 88 |
+
@staticmethod
|
| 89 |
+
def clear_count():
|
| 90 |
+
BaseTrack._count = 0
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def tlwh(self):
|
| 94 |
+
"""Get current position in bounding box format `(top left x, top left y,
|
| 95 |
+
width, height)`.
|
| 96 |
+
mean is the (x_center, y_top, w, h)
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
if self.mean is None:
|
| 100 |
+
return self._tlwh.copy()
|
| 101 |
+
ret = self.mean[:4].copy()
|
| 102 |
+
# ret[:2] -= ret[2:] / 2
|
| 103 |
+
ret[0] -= ret[2] / 2
|
| 104 |
+
return ret
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def tlbr(self):
|
| 108 |
+
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
|
| 109 |
+
`(top left, bottom right)`.
|
| 110 |
+
"""
|
| 111 |
+
ret = self.tlwh.copy()
|
| 112 |
+
ret[2:] += ret[:2]
|
| 113 |
+
return ret
|
| 114 |
+
|
| 115 |
+
@property
|
| 116 |
+
def xywh(self):
|
| 117 |
+
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
|
| 118 |
+
`(top left, bottom right)`.
|
| 119 |
+
"""
|
| 120 |
+
ret = self.tlwh.copy()
|
| 121 |
+
ret[:2] += ret[2:] / 2.0
|
| 122 |
+
return ret
|
| 123 |
+
|
| 124 |
+
@staticmethod
|
| 125 |
+
def tlwh_to_xywh(tlwh):
|
| 126 |
+
"""Convert bounding box to format `(center x, y top, width,
|
| 127 |
+
height)`.
|
| 128 |
+
"""
|
| 129 |
+
ret = np.asarray(tlwh).copy()
|
| 130 |
+
# ret[:2] += ret[2:] / 2
|
| 131 |
+
ret[0] += ret[2] / 2
|
| 132 |
+
return ret
|
| 133 |
+
|
| 134 |
+
def to_xywh(self):
|
| 135 |
+
return self.tlwh_to_xywh(self.tlwh)
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def tlbr_to_tlwh(tlbr):
|
| 139 |
+
ret = np.asarray(tlbr).copy()
|
| 140 |
+
ret[2:] -= ret[:2]
|
| 141 |
+
return ret
|
| 142 |
+
|
| 143 |
+
@staticmethod
|
| 144 |
+
def tlwh_to_tlbr(tlwh):
|
| 145 |
+
ret = np.asarray(tlwh).copy()
|
| 146 |
+
ret[2:] += ret[:2]
|
| 147 |
+
return ret
|
| 148 |
+
|
| 149 |
+
@staticmethod
|
| 150 |
+
def area(tlbr):
|
| 151 |
+
w=tlbr[:,2]-tlbr[:,0]
|
| 152 |
+
h=tlbr[:,3]-tlbr[:,1]
|
| 153 |
+
return w*h
|
| 154 |
+
|
| 155 |
+
@staticmethod
|
| 156 |
+
def height(tlbr):
|
| 157 |
+
h=tlbr[:,3]-tlbr[:,1]
|
| 158 |
+
return h
|
| 159 |
+
|
| 160 |
+
@staticmethod
|
| 161 |
+
def width(tlbr):
|
| 162 |
+
w=tlbr[:,2]-tlbr[:,0]
|
| 163 |
+
return w
|
| 164 |
+
def __repr__(self):
|
| 165 |
+
return 'OT_{}_({}-{})'.format(self.track_id, self.start_frame, self.end_frame)
|
models/models/trackers/reid_parallel_tracker/core/homography.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def compute_Hmat_bev(landmarks):
|
| 5 |
+
assert len(landmarks)==4
|
| 6 |
+
'''
|
| 7 |
+
p1-----p2
|
| 8 |
+
| |
|
| 9 |
+
p3-----p4
|
| 10 |
+
'''
|
| 11 |
+
x1,y1=landmarks[0]
|
| 12 |
+
x2,y2=landmarks[1]
|
| 13 |
+
x3,y3=landmarks[2]
|
| 14 |
+
x4,y4=landmarks[3]
|
| 15 |
+
h13=np.sqrt((x4-x2)**2 +(y4-y2)**2)
|
| 16 |
+
h24=np.sqrt((x3-x1)**2 +(y3-y1)**2)
|
| 17 |
+
new_p1=x1,(y3+y1)/2-h13
|
| 18 |
+
new_p2=x2,(y3+y1)/2-h24
|
| 19 |
+
new_p3=x1,(y3+y1)/2+h13
|
| 20 |
+
new_p4=x2,(y3+y1)/2+h24
|
| 21 |
+
# new_p1=(x3+x1)/2,(y3+y1)/2-h13
|
| 22 |
+
# new_p2=(x2+x4)/2,(y3+y1)/2-h24
|
| 23 |
+
# new_p3=(x3+x1)/2,(y3+y1)/2+h13
|
| 24 |
+
# new_p4=(x2+x4)/2,(y3+y1)/2+h24
|
| 25 |
+
src=np.float32(landmarks)
|
| 26 |
+
dst=np.float32([new_p1,new_p2,new_p3,new_p4])
|
| 27 |
+
#bird's eye view transformation matrix
|
| 28 |
+
Hmat = cv2.getPerspectiveTransform(src, dst) # The transformation matrix
|
| 29 |
+
return Hmat, np.rint(src), np.rint(dst)
|
| 30 |
+
|
| 31 |
+
def compute_tang_alpha(landmarks_bev):
|
| 32 |
+
'''
|
| 33 |
+
p1---p2
|
| 34 |
+
/ \
|
| 35 |
+
p3--------p4
|
| 36 |
+
'''
|
| 37 |
+
x1,y1=landmarks_bev[0]
|
| 38 |
+
x2,y2=landmarks_bev[1]
|
| 39 |
+
x3,y3=landmarks_bev[2]
|
| 40 |
+
x4,y4=landmarks_bev[3]
|
| 41 |
+
ta_left = (x1-x3)/(y3-y1)
|
| 42 |
+
ta_right = (x4-x2)/(y4-y2)
|
| 43 |
+
return ta_left,ta_right
|
| 44 |
+
|
| 45 |
+
def compute_Hmat_left(landmarks):
|
| 46 |
+
assert len(landmarks)==4
|
| 47 |
+
'''
|
| 48 |
+
p1-----p2
|
| 49 |
+
| |
|
| 50 |
+
p3-----p4
|
| 51 |
+
'''
|
| 52 |
+
x1,y1=landmarks[0]
|
| 53 |
+
x2,y2=landmarks[1]
|
| 54 |
+
x3,y3=landmarks[2]
|
| 55 |
+
x4,y4=landmarks[3]
|
| 56 |
+
l12=np.sqrt((x2-x1)**2 +(y2-y1)**2)
|
| 57 |
+
l34=np.sqrt((x4-x3)**2 +(y4-y3)**2)
|
| 58 |
+
h13=np.sqrt((x3-x1)**2 +(y3-y1)**2)
|
| 59 |
+
new_p1=(x1-l12,y3-h13)
|
| 60 |
+
new_p2=(x2,y3-h13)
|
| 61 |
+
new_p3=(x3-l34,y3)
|
| 62 |
+
new_p4=(x4,y3)
|
| 63 |
+
src=np.float32(landmarks)
|
| 64 |
+
dst=np.float32([new_p1,new_p2,new_p3,new_p4])
|
| 65 |
+
Hmat = cv2.getPerspectiveTransform(src, dst) # The transformation matrix
|
| 66 |
+
return Hmat, np.rint(src), np.rint(dst)
|
| 67 |
+
|
| 68 |
+
def compute_Hmat_right(landmarks):
|
| 69 |
+
assert len(landmarks)==4
|
| 70 |
+
'''
|
| 71 |
+
p1-----p2
|
| 72 |
+
| |
|
| 73 |
+
p3-----p4
|
| 74 |
+
'''
|
| 75 |
+
x1,y1=landmarks[0]
|
| 76 |
+
x2,y2=landmarks[1]
|
| 77 |
+
x3,y3=landmarks[2]
|
| 78 |
+
x4,y4=landmarks[3]
|
| 79 |
+
l12=np.sqrt((x2-x1)**2 +(y2-y1)**2)
|
| 80 |
+
l34=np.sqrt((x4-x3)**2 +(y4-y3)**2)
|
| 81 |
+
|
| 82 |
+
new_p1=(x2+x1)/2-l12,y2
|
| 83 |
+
new_p2=(x2+x1)/2+l12,y2
|
| 84 |
+
new_p3=(x3+x4)/2-l34,y4
|
| 85 |
+
new_p4=(x3+x4)/2+l34,y4
|
| 86 |
+
src=np.float32(landmarks)
|
| 87 |
+
dst=np.float32([new_p1,new_p2,new_p3,new_p4])
|
| 88 |
+
Hmat = cv2.getPerspectiveTransform(src, dst) # The transformation matrix
|
| 89 |
+
return Hmat, np.rint(src), np.rint(dst)
|
models/models/trackers/reid_parallel_tracker/core/kalman_filter.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vim: expandtab:ts=4:sw=4
|
| 2 |
+
import numpy as np
|
| 3 |
+
import scipy.linalg
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
Table for the 0.95 quantile of the chi-square distribution with N degrees of
|
| 8 |
+
freedom (contains values for N=1, ..., 9). Taken from MATLAB/Octave's chi2inv
|
| 9 |
+
function and used as Mahalanobis gating threshold.
|
| 10 |
+
"""
|
| 11 |
+
chi2inv95 = {
|
| 12 |
+
1: 3.8415,
|
| 13 |
+
2: 5.9915,
|
| 14 |
+
3: 7.8147,
|
| 15 |
+
4: 9.4877,
|
| 16 |
+
5: 11.070,
|
| 17 |
+
6: 12.592,
|
| 18 |
+
7: 14.067,
|
| 19 |
+
8: 15.507,
|
| 20 |
+
9: 16.919}
|
| 21 |
+
|
| 22 |
+
class KalmanFilter(object):
|
| 23 |
+
"""
|
| 24 |
+
A simple Kalman filter for tracking bounding boxes in image space.
|
| 25 |
+
|
| 26 |
+
The 8-dimensional state space
|
| 27 |
+
|
| 28 |
+
x, y, w, h, vx, vy, vw, vh
|
| 29 |
+
|
| 30 |
+
contains the bounding box center position (x, y), width w, height h,
|
| 31 |
+
and their respective velocities.
|
| 32 |
+
|
| 33 |
+
Object motion follows a constant velocity model. The bounding box location
|
| 34 |
+
(x, y, w, h) is taken as direct observation of the state space (linear
|
| 35 |
+
observation model).
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self,std_pos=1.0/20,std_vel=1./160):
|
| 40 |
+
ndim, dt = 4, 1.
|
| 41 |
+
|
| 42 |
+
# Create Kalman filter model matrices.
|
| 43 |
+
self._motion_mat = np.eye(2 * ndim, 2 * ndim)
|
| 44 |
+
for i in range(ndim):
|
| 45 |
+
self._motion_mat[i, ndim + i] = dt
|
| 46 |
+
self._update_mat = np.eye(ndim, 2 * ndim)
|
| 47 |
+
|
| 48 |
+
# Motion and observation uncertainty are chosen relative to the current
|
| 49 |
+
# state estimate. These weights control the amount of uncertainty in
|
| 50 |
+
# the model. This is a bit hacky.
|
| 51 |
+
self._std_weight_position = std_pos
|
| 52 |
+
self._std_weight_velocity = std_vel
|
| 53 |
+
|
| 54 |
+
def initiate(self, measurement):
|
| 55 |
+
"""Create track from unassociated measurement.
|
| 56 |
+
|
| 57 |
+
Parameters
|
| 58 |
+
----------
|
| 59 |
+
measurement : ndarray
|
| 60 |
+
Bounding box coordinates (x, y, w, h) with center position (x, y),
|
| 61 |
+
width w, and height h.
|
| 62 |
+
|
| 63 |
+
Returns
|
| 64 |
+
-------
|
| 65 |
+
(ndarray, ndarray)
|
| 66 |
+
Returns the mean vector (8 dimensional) and covariance matrix (8x8
|
| 67 |
+
dimensional) of the new track. Unobserved velocities are initialized
|
| 68 |
+
to 0 mean.
|
| 69 |
+
|
| 70 |
+
"""
|
| 71 |
+
mean_pos = measurement
|
| 72 |
+
mean_vel = np.zeros_like(mean_pos)
|
| 73 |
+
mean = np.r_[mean_pos, mean_vel]
|
| 74 |
+
|
| 75 |
+
std = [
|
| 76 |
+
2 * self._std_weight_position * measurement[2],
|
| 77 |
+
2 * self._std_weight_position * measurement[3],
|
| 78 |
+
2 * self._std_weight_position * measurement[2],
|
| 79 |
+
2 * self._std_weight_position * measurement[3],
|
| 80 |
+
10 * self._std_weight_velocity * measurement[2],
|
| 81 |
+
10 * self._std_weight_velocity * measurement[3],
|
| 82 |
+
10 * self._std_weight_velocity * measurement[2],
|
| 83 |
+
10 * self._std_weight_velocity * measurement[3]]
|
| 84 |
+
covariance = np.diag(np.square(std))
|
| 85 |
+
return mean, covariance
|
| 86 |
+
|
| 87 |
+
def predict(self, mean, covariance):
|
| 88 |
+
"""Run Kalman filter prediction step.
|
| 89 |
+
|
| 90 |
+
Parameters
|
| 91 |
+
----------
|
| 92 |
+
mean : ndarray
|
| 93 |
+
The 8 dimensional mean vector of the object state at the previous
|
| 94 |
+
time step.
|
| 95 |
+
covariance : ndarray
|
| 96 |
+
The 8x8 dimensional covariance matrix of the object state at the
|
| 97 |
+
previous time step.
|
| 98 |
+
|
| 99 |
+
Returns
|
| 100 |
+
-------
|
| 101 |
+
(ndarray, ndarray)
|
| 102 |
+
Returns the mean vector and covariance matrix of the predicted
|
| 103 |
+
state. Unobserved velocities are initialized to 0 mean.
|
| 104 |
+
|
| 105 |
+
"""
|
| 106 |
+
std_pos = [
|
| 107 |
+
self._std_weight_position * mean[2],
|
| 108 |
+
self._std_weight_position * mean[3],
|
| 109 |
+
self._std_weight_position * mean[2],
|
| 110 |
+
self._std_weight_position * mean[3]]
|
| 111 |
+
std_vel = [
|
| 112 |
+
self._std_weight_velocity * mean[2],
|
| 113 |
+
self._std_weight_velocity * mean[3],
|
| 114 |
+
self._std_weight_velocity * mean[2],
|
| 115 |
+
self._std_weight_velocity * mean[3]]
|
| 116 |
+
motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
|
| 117 |
+
|
| 118 |
+
mean = np.dot(mean, self._motion_mat.T)
|
| 119 |
+
covariance = np.linalg.multi_dot((
|
| 120 |
+
self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
|
| 121 |
+
|
| 122 |
+
return mean, covariance
|
| 123 |
+
|
| 124 |
+
def project(self, mean, covariance, detection_score=0.5):
|
| 125 |
+
"""Project state distribution to measurement space.
|
| 126 |
+
|
| 127 |
+
Parameters
|
| 128 |
+
----------
|
| 129 |
+
mean : ndarray
|
| 130 |
+
The state's mean vector (8 dimensional array).
|
| 131 |
+
covariance : ndarray
|
| 132 |
+
The state's covariance matrix (8x8 dimensional).
|
| 133 |
+
|
| 134 |
+
Returns
|
| 135 |
+
-------
|
| 136 |
+
(ndarray, ndarray)
|
| 137 |
+
Returns the projected mean and covariance matrix of the given state
|
| 138 |
+
estimate.
|
| 139 |
+
|
| 140 |
+
"""
|
| 141 |
+
# we increase the std of height/width because objects may be occluded.
|
| 142 |
+
std = [
|
| 143 |
+
self._std_weight_position * mean[2],
|
| 144 |
+
self._std_weight_position * mean[3],
|
| 145 |
+
2*self._std_weight_position * mean[2],
|
| 146 |
+
2*self._std_weight_position * mean[3]]
|
| 147 |
+
innovation_cov = np.diag(2.*(1-detection_score)*np.square(std))
|
| 148 |
+
# innovation_cov = np.diag(np.square(std))
|
| 149 |
+
|
| 150 |
+
mean = np.dot(self._update_mat, mean)
|
| 151 |
+
covariance = np.linalg.multi_dot((
|
| 152 |
+
self._update_mat, covariance, self._update_mat.T))
|
| 153 |
+
return mean, covariance + innovation_cov
|
| 154 |
+
|
| 155 |
+
def multi_predict(self, mean, covariance):
|
| 156 |
+
"""Run Kalman filter prediction step (Vectorized version).
|
| 157 |
+
Parameters
|
| 158 |
+
----------
|
| 159 |
+
mean : ndarray
|
| 160 |
+
The Nx8 dimensional mean matrix of the object states at the previous
|
| 161 |
+
time step.
|
| 162 |
+
covariance : ndarray
|
| 163 |
+
The Nx8x8 dimensional covariance matrics of the object states at the
|
| 164 |
+
previous time step.
|
| 165 |
+
Returns
|
| 166 |
+
-------
|
| 167 |
+
(ndarray, ndarray)
|
| 168 |
+
Returns the mean vector and covariance matrix of the predicted
|
| 169 |
+
state. Unobserved velocities are initialized to 0 mean.
|
| 170 |
+
"""
|
| 171 |
+
mean32 = mean[:, 2]
|
| 172 |
+
mean33 = mean[:, 3]
|
| 173 |
+
std_pos = [
|
| 174 |
+
self._std_weight_position * mean32,
|
| 175 |
+
self._std_weight_position * mean33,
|
| 176 |
+
self._std_weight_position * mean32,
|
| 177 |
+
self._std_weight_position * mean33
|
| 178 |
+
]
|
| 179 |
+
std_vel = [
|
| 180 |
+
self._std_weight_velocity * mean32,
|
| 181 |
+
self._std_weight_velocity * mean33,
|
| 182 |
+
self._std_weight_velocity * mean32,
|
| 183 |
+
self._std_weight_velocity * mean33
|
| 184 |
+
]
|
| 185 |
+
sqr = np.square(np.concatenate((std_pos, std_vel)).T)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
motion_cov = []
|
| 189 |
+
for i in range(len(mean)):
|
| 190 |
+
motion_cov.append(np.diag(sqr[i]))
|
| 191 |
+
motion_cov = np.asarray(motion_cov)
|
| 192 |
+
|
| 193 |
+
mean = np.dot(mean, self._motion_mat.T)
|
| 194 |
+
left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
|
| 195 |
+
covariance = np.dot(left, self._motion_mat.T) + motion_cov
|
| 196 |
+
|
| 197 |
+
return mean, covariance
|
| 198 |
+
|
| 199 |
+
def update(self, mean, covariance, measurement, detection_score=0.5):
|
| 200 |
+
"""Run Kalman filter correction step.
|
| 201 |
+
|
| 202 |
+
Parameters
|
| 203 |
+
----------
|
| 204 |
+
mean : ndarray
|
| 205 |
+
The predicted state's mean vector (8 dimensional).
|
| 206 |
+
covariance : ndarray
|
| 207 |
+
The state's covariance matrix (8x8 dimensional).
|
| 208 |
+
measurement : ndarray
|
| 209 |
+
The 4 dimensional measurement vector (x, y, w, h), where (x, y)
|
| 210 |
+
is the center position, w the width, and h the height of the
|
| 211 |
+
bounding box.
|
| 212 |
+
|
| 213 |
+
Returns
|
| 214 |
+
-------
|
| 215 |
+
(ndarray, ndarray)
|
| 216 |
+
Returns the measurement-corrected state distribution.
|
| 217 |
+
|
| 218 |
+
"""
|
| 219 |
+
projected_mean, projected_cov = self.project(mean, covariance, detection_score)
|
| 220 |
+
|
| 221 |
+
chol_factor, lower = scipy.linalg.cho_factor(
|
| 222 |
+
projected_cov, lower=True, check_finite=False)
|
| 223 |
+
kalman_gain = scipy.linalg.cho_solve(
|
| 224 |
+
(chol_factor, lower), np.dot(covariance, self._update_mat.T).T,
|
| 225 |
+
check_finite=False).T
|
| 226 |
+
innovation = measurement - projected_mean
|
| 227 |
+
|
| 228 |
+
new_mean = mean + np.dot(innovation, kalman_gain.T)
|
| 229 |
+
new_covariance = covariance - np.linalg.multi_dot((
|
| 230 |
+
kalman_gain, projected_cov, kalman_gain.T))
|
| 231 |
+
return new_mean, new_covariance
|
| 232 |
+
|
| 233 |
+
def gating_distance(self, mean, covariance, measurements,
|
| 234 |
+
only_position=False, metric='maha'):
|
| 235 |
+
"""Compute gating distance between state distribution and measurements.
|
| 236 |
+
A suitable distance threshold can be obtained from `chi2inv95`. If
|
| 237 |
+
`only_position` is False, the chi-square distribution has 4 degrees of
|
| 238 |
+
freedom, otherwise 2.
|
| 239 |
+
Parameters
|
| 240 |
+
----------
|
| 241 |
+
mean : ndarray
|
| 242 |
+
Mean vector over the state distribution (8 dimensional).
|
| 243 |
+
covariance : ndarray
|
| 244 |
+
Covariance of the state distribution (8x8 dimensional).
|
| 245 |
+
measurements : ndarray
|
| 246 |
+
An Nx4 dimensional matrix of N measurements, each in
|
| 247 |
+
format (x, y, a, h) where (x, y) is the bounding box center
|
| 248 |
+
position, a the aspect ratio, and h the height.
|
| 249 |
+
only_position : Optional[bool]
|
| 250 |
+
If True, distance computation is done with respect to the bounding
|
| 251 |
+
box center position only.
|
| 252 |
+
Returns
|
| 253 |
+
-------
|
| 254 |
+
ndarray
|
| 255 |
+
Returns an array of length N, where the i-th element contains the
|
| 256 |
+
squared Mahalanobis distance between (mean, covariance) and
|
| 257 |
+
`measurements[i]`.
|
| 258 |
+
"""
|
| 259 |
+
mean, covariance = self.project(mean, covariance)
|
| 260 |
+
if only_position:
|
| 261 |
+
mean, covariance = mean[:2], covariance[:2, :2]
|
| 262 |
+
measurements = measurements[:, :2]
|
| 263 |
+
|
| 264 |
+
d = measurements - mean
|
| 265 |
+
if metric == 'gaussian':
|
| 266 |
+
return np.sum(d * d, axis=1)
|
| 267 |
+
elif metric == 'maha':
|
| 268 |
+
cholesky_factor = np.linalg.cholesky(covariance)
|
| 269 |
+
z = scipy.linalg.solve_triangular(
|
| 270 |
+
cholesky_factor, d.T, lower=True, check_finite=False,
|
| 271 |
+
overwrite_b=True)
|
| 272 |
+
squared_maha = np.sum(z * z, axis=0)
|
| 273 |
+
return squared_maha
|
| 274 |
+
else:
|
| 275 |
+
raise ValueError('invalid distance metric')
|
models/models/trackers/reid_parallel_tracker/core/matching.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import scipy
|
| 3 |
+
import lap
|
| 4 |
+
import torch
|
| 5 |
+
from scipy.spatial.distance import cdist
|
| 6 |
+
from numpy.linalg import norm
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
from mmdet.evaluation.functional import bbox_overlaps
|
| 9 |
+
# from cython_bbox import bbox_overlaps as bbox_ious
|
| 10 |
+
|
| 11 |
+
DEG2RAD= np.pi/180
|
| 12 |
+
|
| 13 |
+
def merge_matches(m1, m2, shape):
|
| 14 |
+
O,P,Q = shape
|
| 15 |
+
m1 = np.asarray(m1)
|
| 16 |
+
m2 = np.asarray(m2)
|
| 17 |
+
|
| 18 |
+
M1 = scipy.sparse.coo_matrix((np.ones(len(m1)), (m1[:, 0], m1[:, 1])), shape=(O, P))
|
| 19 |
+
M2 = scipy.sparse.coo_matrix((np.ones(len(m2)), (m2[:, 0], m2[:, 1])), shape=(P, Q))
|
| 20 |
+
|
| 21 |
+
mask = M1*M2
|
| 22 |
+
match = mask.nonzero()
|
| 23 |
+
match = list(zip(match[0], match[1]))
|
| 24 |
+
unmatched_O = tuple(set(range(O)) - set([i for i, j in match]))
|
| 25 |
+
unmatched_Q = tuple(set(range(Q)) - set([j for i, j in match]))
|
| 26 |
+
|
| 27 |
+
return match, unmatched_O, unmatched_Q
|
| 28 |
+
|
| 29 |
+
def linear_assignment(cost_matrix, thresh):
|
| 30 |
+
if cost_matrix.size == 0:
|
| 31 |
+
return np.empty((0, 2), dtype=int), np.array(range(cost_matrix.shape[0]), dtype=int), np.array(range(cost_matrix.shape[1]), dtype=int)
|
| 32 |
+
matches, unmatched_a, unmatched_b = [], [], []
|
| 33 |
+
cost, x, y = lap.lapjv(cost_matrix, extend_cost=True, cost_limit=thresh)
|
| 34 |
+
for ix, mx in enumerate(x):
|
| 35 |
+
if mx >= 0:
|
| 36 |
+
matches.append([ix, mx])
|
| 37 |
+
unmatched_a = np.where(x < 0)[0]
|
| 38 |
+
unmatched_b = np.where(y < 0)[0]
|
| 39 |
+
return np.asarray(matches, dtype=int).reshape(-1, 2), np.array(unmatched_a, dtype=int), np.array(unmatched_b, dtype=int)
|
| 40 |
+
|
| 41 |
+
def topk_assignment(cost_matrix:np.ndarray,
|
| 42 |
+
thresh: float,
|
| 43 |
+
tolerance: float=None,
|
| 44 |
+
tolerance_ratio: float=0.15,
|
| 45 |
+
topk: int=1):
|
| 46 |
+
"""_summary_
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
cost_matrix (np.ndarray): [num_tracks x num_dets]
|
| 50 |
+
thresh (float): maximum distance threshold
|
| 51 |
+
tolerance (float, optional): the maximum distance between the best and second matches. Defaults to 0.05.
|
| 52 |
+
topk (int, optional):topk matching. Defaults to 1.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
_type_: _description_
|
| 56 |
+
"""
|
| 57 |
+
if cost_matrix.size == 0:
|
| 58 |
+
return np.empty((0, 2), dtype=int), np.array(range(cost_matrix.shape[0]), dtype=int), np.array(range(cost_matrix.shape[1]), dtype=int)
|
| 59 |
+
|
| 60 |
+
topk=min(topk,cost_matrix.shape[1])
|
| 61 |
+
if topk==1:
|
| 62 |
+
return linear_assignment(cost_matrix, thresh)
|
| 63 |
+
|
| 64 |
+
tolerance = tolerance or tolerance_ratio*thresh
|
| 65 |
+
# select topk cols has lowest cost value for each row
|
| 66 |
+
match_idx = np.argsort(cost_matrix, axis=1)[:, :topk]
|
| 67 |
+
match_scores = cost_matrix[np.arange(cost_matrix.shape[0])[:, None], match_idx]
|
| 68 |
+
valid_match = (match_scores < thresh) & ((match_scores-match_scores[:,0:1]) <= tolerance)
|
| 69 |
+
|
| 70 |
+
match_cols = match_idx[valid_match]
|
| 71 |
+
unmatched_a = np.where(np.sum(valid_match, axis=1) == 0)[0]
|
| 72 |
+
unmatched_b = np.array([i for i in range(cost_matrix.shape[1]) if i not in match_cols])
|
| 73 |
+
|
| 74 |
+
# add index of rows
|
| 75 |
+
match_rows = np.repeat(np.arange(cost_matrix.shape[0])[:,None],topk,1)
|
| 76 |
+
match_rows = match_rows[valid_match]
|
| 77 |
+
|
| 78 |
+
matches = np.array([match_rows,match_cols]).T
|
| 79 |
+
return np.asarray(matches, dtype=int), np.array(unmatched_a, dtype=int), np.array(unmatched_b, dtype=int)
|
| 80 |
+
|
| 81 |
+
def center_distance(track, det):
|
| 82 |
+
diff = track.xywh[:2] - det.xywh[:2]
|
| 83 |
+
# return L2 norm
|
| 84 |
+
return norm(diff, ord=2)
|
| 85 |
+
|
| 86 |
+
def ious(atlbrs, btlbrs):
|
| 87 |
+
"""
|
| 88 |
+
Compute cost based on IoU
|
| 89 |
+
:type atlbrs: list[tlbr] | np.ndarray
|
| 90 |
+
:type atlbrs: list[tlbr] | np.ndarray
|
| 91 |
+
|
| 92 |
+
:rtype ious np.ndarray
|
| 93 |
+
"""
|
| 94 |
+
ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
|
| 95 |
+
if ious.size == 0:
|
| 96 |
+
return ious
|
| 97 |
+
|
| 98 |
+
# ious = bbox_ious(
|
| 99 |
+
ious = bbox_overlaps(
|
| 100 |
+
np.ascontiguousarray(atlbrs, dtype=np.float32),
|
| 101 |
+
np.ascontiguousarray(btlbrs, dtype=np.float32)
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
return ious
|
| 105 |
+
|
| 106 |
+
def ious_adaptive_height(atlbrs, btlbrs):
|
| 107 |
+
ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
|
| 108 |
+
if ious.size == 0:
|
| 109 |
+
return ious
|
| 110 |
+
b_tlbrs = deepcopy(btlbrs)
|
| 111 |
+
_ious=[]
|
| 112 |
+
for a_tlbr in atlbrs:
|
| 113 |
+
ha_ = a_tlbr[3]-a_tlbr[1]
|
| 114 |
+
# Adjust the height in b_
|
| 115 |
+
for b_tlbr in b_tlbrs: b_tlbr[3] = b_tlbr[1]+ha_
|
| 116 |
+
a_ious = bbox_overlaps(
|
| 117 |
+
np.ascontiguousarray([a_tlbr], dtype=np.float32),
|
| 118 |
+
np.ascontiguousarray(b_tlbrs, dtype=np.float32)
|
| 119 |
+
)
|
| 120 |
+
_ious.append(a_ious)
|
| 121 |
+
_ious = np.concatenate(_ious,axis=0)
|
| 122 |
+
return _ious
|
| 123 |
+
|
| 124 |
+
def tlbr_expand(tlbr, scale=1.2):
|
| 125 |
+
w = tlbr[2] - tlbr[0]
|
| 126 |
+
h = tlbr[3] - tlbr[1]
|
| 127 |
+
|
| 128 |
+
half_scale = 0.5 * (scale-1)
|
| 129 |
+
tlbr[0] -= half_scale * w
|
| 130 |
+
tlbr[1] -= half_scale * h
|
| 131 |
+
tlbr[2] += half_scale * w
|
| 132 |
+
tlbr[3] += half_scale * h
|
| 133 |
+
|
| 134 |
+
return tlbr
|
| 135 |
+
|
| 136 |
+
def iou_scores(atracks, btracks, adaptive_height=False):
|
| 137 |
+
"""
|
| 138 |
+
Compute cost based on IoU
|
| 139 |
+
:type atracks: list[Tracklet]
|
| 140 |
+
:type btracks: list[Tracklet]
|
| 141 |
+
|
| 142 |
+
:rtype cost_matrix np.ndarray
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
if (len(atracks)>0 and isinstance(atracks[0], np.ndarray)) or (len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
|
| 146 |
+
atlbrs = atracks
|
| 147 |
+
btlbrs = btracks
|
| 148 |
+
else:
|
| 149 |
+
atlbrs = [track.tlbr for track in atracks]
|
| 150 |
+
btlbrs = [track.tlbr for track in btracks]
|
| 151 |
+
_ious = ious_adaptive_height(atlbrs, btlbrs) if adaptive_height else ious(atlbrs, btlbrs)
|
| 152 |
+
return _ious
|
| 153 |
+
|
| 154 |
+
def vel_consistent_scores(tracks,detections):
|
| 155 |
+
if (len(tracks)>0 and isinstance(tracks[0], np.ndarray)) or (len(detections) > 0 and isinstance(detections[0], np.ndarray)):
|
| 156 |
+
atlbrs = tracks
|
| 157 |
+
btlbrs = detections
|
| 158 |
+
else:
|
| 159 |
+
atlbrs = np.array([track.tlbr for track in tracks])
|
| 160 |
+
btlbrs = np.array([track.tlbr for track in detections])
|
| 161 |
+
if len(atlbrs) == 0 or len(btlbrs) == 0 :
|
| 162 |
+
return np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
|
| 163 |
+
|
| 164 |
+
# Current velocity direction
|
| 165 |
+
vel_dir_cur = np.array([s.vel_dir for s in tracks])
|
| 166 |
+
vel_dir_cur = np.expand_dims(vel_dir_cur,axis=1)
|
| 167 |
+
# Compute expected velocity direction
|
| 168 |
+
xa,ya = atlbrs[:,0] + atlbrs[:,2]/2,atlbrs[:,1]
|
| 169 |
+
xb,yb = btlbrs[:,0] + btlbrs[:,2]/2,btlbrs[:,1]
|
| 170 |
+
dx = xb[None,:]-xa[:,None]
|
| 171 |
+
dy = yb[None,:]-ya[:,None]
|
| 172 |
+
norm = np.sqrt(dx*dx + dy*dy) +1e-6
|
| 173 |
+
vel_dir = np.stack([dx/norm,dy/norm],axis=2)
|
| 174 |
+
|
| 175 |
+
vel_scores = np.sum(vel_dir_cur*vel_dir,axis=2)
|
| 176 |
+
return vel_scores
|
| 177 |
+
|
| 178 |
+
def ort_consistent_scores(tracks,detections):
|
| 179 |
+
a_angles = np.array([track.angle for track in tracks])
|
| 180 |
+
b_angles = np.array([track.angle for track in detections])
|
| 181 |
+
if len(a_angles) == 0 or len(b_angles) == 0 :
|
| 182 |
+
return np.zeros((len(a_angles), len(b_angles)), dtype=np.float32)
|
| 183 |
+
diff = np.absolute(a_angles[:,None] - b_angles[None,:])
|
| 184 |
+
diff = np.minimum(diff,360-diff)
|
| 185 |
+
return np.cos(diff*DEG2RAD)
|
| 186 |
+
|
| 187 |
+
def expand_iou_scores(atracks, btracks,expand_scale=1.2):
|
| 188 |
+
"""
|
| 189 |
+
Compute cost based on IoU
|
| 190 |
+
:type atracks: list[Tracklet]
|
| 191 |
+
:type btracks: list[Tracklet]
|
| 192 |
+
|
| 193 |
+
:rtype cost_matrix np.ndarray
|
| 194 |
+
"""
|
| 195 |
+
if (len(atracks)>0 and isinstance(atracks[0], np.ndarray)) or (len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
|
| 196 |
+
atlbrs = atracks
|
| 197 |
+
btlbrs = btracks
|
| 198 |
+
else:
|
| 199 |
+
atlbrs = [tlbr_expand(track.tlbr, scale=expand_scale) for track in atracks]
|
| 200 |
+
btlbrs = [tlbr_expand(track.tlbr, scale=expand_scale) for track in btracks]
|
| 201 |
+
_ious = ious(atlbrs, btlbrs)
|
| 202 |
+
return _ious
|
| 203 |
+
|
| 204 |
+
def embedding_distance(tracks, detections, metric='cosine'):
|
| 205 |
+
"""
|
| 206 |
+
:param tracks: list[Tracklet]
|
| 207 |
+
:param detections: list[BaseTrack]
|
| 208 |
+
:param metric:
|
| 209 |
+
:return: cost_matrix np.ndarray
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
cost_matrix = np.zeros((len(tracks), len(detections)), dtype=np.float32)
|
| 213 |
+
if cost_matrix.size == 0:
|
| 214 |
+
return cost_matrix
|
| 215 |
+
det_features = np.asarray([track.curr_feat for track in detections], dtype=np.float32)
|
| 216 |
+
track_features = np.asarray([track.smooth_feat for track in tracks], dtype=np.float32)
|
| 217 |
+
|
| 218 |
+
cost_matrix = np.maximum(0.0, cdist(track_features, det_features, metric)) # / 2.0 # Nomalized features
|
| 219 |
+
return cost_matrix
|
| 220 |
+
|
| 221 |
+
def embedding_cosine_distance(tracklets, dets, feature_type='curr_feat', norm=False):
|
| 222 |
+
# embedding distance base on cosine similarity
|
| 223 |
+
|
| 224 |
+
# if tracklets and dets are already embeddings
|
| 225 |
+
if ((len(tracklets)>0 and isinstance(tracklets[0], np.ndarray))
|
| 226 |
+
or (len(dets) > 0 and isinstance(dets[0], np.ndarray))):
|
| 227 |
+
track_feats = tracklets
|
| 228 |
+
det_feats = dets
|
| 229 |
+
else:
|
| 230 |
+
# else, take the embedding from tracklet class
|
| 231 |
+
track_feats = [getattr(track, feature_type) for track in tracklets]
|
| 232 |
+
det_feats = [getattr(det, 'curr_feat') for det in dets]
|
| 233 |
+
# cal culate distance base on cosine
|
| 234 |
+
if len(track_feats) > 0 and len(det_feats) > 0:
|
| 235 |
+
track_feats = np.stack(track_feats)
|
| 236 |
+
det_feats = np.stack(det_feats)
|
| 237 |
+
if norm:
|
| 238 |
+
track_feats = track_feats / np.linalg.norm(track_feats, axis=1)[:, None]
|
| 239 |
+
det_feats = det_feats / np.linalg.norm(det_feats, axis=1)[:, None]
|
| 240 |
+
cosine = np.matmul(track_feats, det_feats.T)
|
| 241 |
+
return 1 - cosine
|
| 242 |
+
else:
|
| 243 |
+
return np.empty((len(track_feats), len(det_feats)), dtype=np.float64)
|
| 244 |
+
|
| 245 |
+
def euclidean_distance_matrix(atracks, btracks):
|
| 246 |
+
"""
|
| 247 |
+
Calculates the Euclidean distance matrix between middle top left points of atracks and btracks
|
| 248 |
+
|
| 249 |
+
:param atracks: A list of tracklets or atracks representing the first set of track data.
|
| 250 |
+
:type atracks: list[Tracklet] size N
|
| 251 |
+
:param btracks: A list of tracklets or atracks representing the second set of track data.
|
| 252 |
+
:type btracks: list[Tracklet] size M
|
| 253 |
+
|
| 254 |
+
:return: a dictionary
|
| 255 |
+
- distance_matrix size NxM: A numpy.ndarray representing the Euclidean distance between each pair of tracklets.
|
| 256 |
+
- width_atracks size Nx1: A numpy.ndarray indicating widths of boxes in atracks
|
| 257 |
+
:rtype: dict
|
| 258 |
+
"""
|
| 259 |
+
if len(atracks) == 0 or len(btracks) == 0:
|
| 260 |
+
emtyp_matrix = np.empty((len(atracks), len(btracks)), dtype=np.float32)
|
| 261 |
+
return dict(distance_matrix=emtyp_matrix, width_atracks=emtyp_matrix)
|
| 262 |
+
|
| 263 |
+
if (len(atracks)>0 and isinstance(atracks[0], np.ndarray)) or (len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
|
| 264 |
+
atlbrs = atracks
|
| 265 |
+
btlbrs = btracks
|
| 266 |
+
else:
|
| 267 |
+
atlbrs = [track.tlbr for track in atracks]
|
| 268 |
+
btlbrs = [track.tlbr for track in btracks]
|
| 269 |
+
|
| 270 |
+
# Extract the middle top points from atlbrs and btlbrs
|
| 271 |
+
atlbrs_midtop = np.array([((bbox[0] + bbox[2]) / 2, bbox[1]) for bbox in atlbrs])
|
| 272 |
+
btlbrs_midtop = np.array([((bbox[0] + bbox[2]) / 2, bbox[1]) for bbox in btlbrs])
|
| 273 |
+
|
| 274 |
+
# Calculate the Euclidean distance between each pair of middle top points
|
| 275 |
+
distance_matrix = np.sqrt(np.sum((atlbrs_midtop[:, np.newaxis] - btlbrs_midtop) ** 2, axis=-1))
|
| 276 |
+
|
| 277 |
+
# evaluate distance_matrix valid or not
|
| 278 |
+
width_atracks = np.array([(bbox[2] - bbox[0]) / 2 for bbox in atlbrs])[:, None]
|
| 279 |
+
|
| 280 |
+
return dict(distance_matrix=distance_matrix, width_atracks=width_atracks)
|
| 281 |
+
|
| 282 |
+
# def gate_cost_matrix(kf, cost_matrix, tracks, detections, only_position=False):
|
| 283 |
+
# if cost_matrix.size == 0:
|
| 284 |
+
# return cost_matrix
|
| 285 |
+
# gating_dim = 2 if only_position else 4
|
| 286 |
+
# gating_threshold = kalman_filter.chi2inv95[gating_dim]
|
| 287 |
+
# # measurements = np.asarray([det.to_xyah() for det in detections])
|
| 288 |
+
# measurements = np.asarray([det.to_xywh() for det in detections])
|
| 289 |
+
# for row, track in enumerate(tracks):
|
| 290 |
+
# gating_distance = kf.gating_distance(
|
| 291 |
+
# track.mean, track.covariance, measurements, only_position)
|
| 292 |
+
# cost_matrix[row, gating_distance > gating_threshold] = np.inf
|
| 293 |
+
# return cost_matrix
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# def fuse_motion(kf, cost_matrix, tracks, detections, only_position=False, lambda_=0.98):
|
| 297 |
+
# if cost_matrix.size == 0:
|
| 298 |
+
# return cost_matrix
|
| 299 |
+
# gating_dim = 2 if only_position else 4
|
| 300 |
+
# gating_threshold = kalman_filter.chi2inv95[gating_dim]
|
| 301 |
+
# # measurements = np.asarray([det.to_xyah() for det in detections])
|
| 302 |
+
# measurements = np.asarray([det.to_xywh() for det in detections])
|
| 303 |
+
# for row, track in enumerate(tracks):
|
| 304 |
+
# gating_distance = kf.gating_distance(
|
| 305 |
+
# track.mean, track.covariance, measurements, only_position, metric='maha')
|
| 306 |
+
# cost_matrix[row, gating_distance > gating_threshold] = np.inf
|
| 307 |
+
# cost_matrix[row] = lambda_ * cost_matrix[row] + (1 - lambda_) * gating_distance
|
| 308 |
+
# return cost_matrix
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def fuse_iou(cost_matrix, tracks, detections):
|
| 312 |
+
if cost_matrix.size == 0:
|
| 313 |
+
return cost_matrix
|
| 314 |
+
reid_sim = 1 - cost_matrix
|
| 315 |
+
iou_dist = 1 -iou_scores(tracks, detections)
|
| 316 |
+
iou_sim = 1 - iou_dist
|
| 317 |
+
fuse_sim = reid_sim * (1 + iou_sim) / 2
|
| 318 |
+
det_scores = np.array([det.score for det in detections])
|
| 319 |
+
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
|
| 320 |
+
#fuse_sim = fuse_sim * (1 + det_scores) / 2
|
| 321 |
+
fuse_cost = 1 - fuse_sim
|
| 322 |
+
return fuse_cost
|
| 323 |
+
|
| 324 |
+
def fuse_score(cost_matrix, detections):
|
| 325 |
+
if cost_matrix.size == 0:
|
| 326 |
+
return cost_matrix
|
| 327 |
+
iou_sim = 1 - cost_matrix
|
| 328 |
+
det_scores = np.array([det.score for det in detections])
|
| 329 |
+
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
|
| 330 |
+
fuse_sim = iou_sim * det_scores
|
| 331 |
+
fuse_cost = 1 - fuse_sim
|
| 332 |
+
return fuse_cost
|
| 333 |
+
|
| 334 |
+
def solider_distance(tracklets, dets, feature_type='curr_feat', norm=True):
|
| 335 |
+
# if tracklets and dets are already embeddings
|
| 336 |
+
if ((len(tracklets)>0 and isinstance(tracklets[0], np.ndarray))
|
| 337 |
+
or (len(dets) > 0 and isinstance(dets[0], np.ndarray))):
|
| 338 |
+
track_feats = tracklets
|
| 339 |
+
det_feats = dets
|
| 340 |
+
else:
|
| 341 |
+
# else, take the embedding from tracklet class
|
| 342 |
+
track_feats = [getattr(track, feature_type) for track in tracklets]
|
| 343 |
+
det_feats = [getattr(det, 'curr_feat') for det in dets]
|
| 344 |
+
# cal culate distance base on cosine
|
| 345 |
+
if len(track_feats) > 0 and len(det_feats) > 0:
|
| 346 |
+
track_feats = np.stack(track_feats)
|
| 347 |
+
det_feats = np.stack(det_feats)
|
| 348 |
+
|
| 349 |
+
qf = torch.from_numpy(track_feats).to('cuda')
|
| 350 |
+
gf = torch.from_numpy(det_feats).to('cuda')
|
| 351 |
+
if norm:
|
| 352 |
+
qf = torch.nn.functional.normalize(qf, dim=1, p=2)
|
| 353 |
+
gf = torch.nn.functional.normalize(gf, dim=1, p=2)
|
| 354 |
+
m = qf.shape[0]
|
| 355 |
+
n = gf.shape[0]
|
| 356 |
+
dist_mat = torch.pow(qf, 2).sum(dim=1, keepdim=True).expand(m, n) + \
|
| 357 |
+
torch.pow(gf, 2).sum(dim=1, keepdim=True).expand(n, m).t()
|
| 358 |
+
dist_mat.addmm_(1, -2, qf, gf.t())
|
| 359 |
+
return dist_mat.cpu().numpy()
|
| 360 |
+
else:
|
| 361 |
+
return np.empty((len(track_feats), len(det_feats)), dtype=np.float64)
|
| 362 |
+
|
| 363 |
+
def best_area_solider_distance(tracklets, dets, norm=False):
|
| 364 |
+
|
| 365 |
+
if len(tracklets) > 0 and len(dets) > 0:
|
| 366 |
+
# if tracklets and dets are already embeddings
|
| 367 |
+
if ((len(tracklets)>0 and isinstance(tracklets[0], np.ndarray))
|
| 368 |
+
or (len(dets) > 0 and isinstance(dets[0], np.ndarray))):
|
| 369 |
+
track_feats = tracklets
|
| 370 |
+
det_feats = dets
|
| 371 |
+
else:
|
| 372 |
+
# else, take the embedding from tracklet class
|
| 373 |
+
det_feats = [getattr(det, 'curr_feat') for det in dets]
|
| 374 |
+
|
| 375 |
+
# take all best match feat based on det box area
|
| 376 |
+
track_feats = []
|
| 377 |
+
for track_idx, tracklet in enumerate(tracklets):
|
| 378 |
+
for det_idx, det in enumerate(dets):
|
| 379 |
+
best_match_results = tracklet.best_area_feat(det._tlwh)
|
| 380 |
+
feat = best_match_results['feat']
|
| 381 |
+
track_feats.append(feat)
|
| 382 |
+
# MxNxC
|
| 383 |
+
track_feats = np.stack(track_feats).reshape(len(tracklets), len(dets), -1)
|
| 384 |
+
# NxC
|
| 385 |
+
det_feats = np.stack(det_feats)
|
| 386 |
+
|
| 387 |
+
qf = torch.from_numpy(track_feats).to('cuda')
|
| 388 |
+
gf = torch.from_numpy(det_feats).to('cuda')
|
| 389 |
+
|
| 390 |
+
if norm:
|
| 391 |
+
qf = torch.nn.functional.normalize(qf, dim=-1, p=2)
|
| 392 |
+
gf = torch.nn.functional.normalize(gf, dim=-1, p=2)
|
| 393 |
+
|
| 394 |
+
# m = qf.shape[0]
|
| 395 |
+
# n = gf.shape[0]
|
| 396 |
+
# dist_mat = torch.pow(qf, 2).sum(dim=-1, keepdim=False) + \
|
| 397 |
+
# torch.pow(gf, 2).sum(dim=1, keepdim=True).expand(n, m).t()
|
| 398 |
+
# dist_mat.addmm_(1, -2, qf, gf.t())
|
| 399 |
+
|
| 400 |
+
gf = gf.unsqueeze(0)
|
| 401 |
+
dist_matff = ((qf-gf)**2).sum(-1) #.sqrt()
|
| 402 |
+
|
| 403 |
+
return dist_matff.cpu().numpy()
|
| 404 |
+
else:
|
| 405 |
+
return np.empty((len(tracklets), len(dets)), dtype=np.float64)
|
models/models/trackers/reid_parallel_tracker/core/tracklet.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import numpy as np
|
| 3 |
+
from collections import deque
|
| 4 |
+
from .matching import iou_scores
|
| 5 |
+
from .basetrack import BaseTrack, TrackState
|
| 6 |
+
from .kalman_filter import KalmanFilter
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
import cv2
|
| 9 |
+
|
| 10 |
+
RAD2DEG = 180.0/np.pi
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def add_stracks(tlista, tlistb):
|
| 14 |
+
exists = {}
|
| 15 |
+
res = []
|
| 16 |
+
for t in tlista:
|
| 17 |
+
exists[t.track_id] = 1
|
| 18 |
+
res.append(t)
|
| 19 |
+
for t in tlistb:
|
| 20 |
+
tid = t.track_id
|
| 21 |
+
if not exists.get(tid, 0):
|
| 22 |
+
exists[tid] = 1
|
| 23 |
+
res.append(t)
|
| 24 |
+
return res
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def subtract_stracks(tlista, tlistb):
|
| 28 |
+
stracks = {}
|
| 29 |
+
for t in tlista:
|
| 30 |
+
stracks[t.track_id] = t
|
| 31 |
+
for t in tlistb:
|
| 32 |
+
tid = t.track_id
|
| 33 |
+
if stracks.get(tid, 0):
|
| 34 |
+
del stracks[tid]
|
| 35 |
+
return list(stracks.values())
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def remove_duplicate_stracks(stracksa, stracksb):
|
| 39 |
+
pscores = iou_scores(stracksa, stracksb)
|
| 40 |
+
pairs = np.where(pscores > 0.85)
|
| 41 |
+
dupa, dupb = list(), list()
|
| 42 |
+
for p, q in zip(*pairs):
|
| 43 |
+
timep = stracksa[p].frame_id - stracksa[p].start_frame
|
| 44 |
+
timeq = stracksb[q].frame_id - stracksb[q].start_frame
|
| 45 |
+
if timep > timeq:
|
| 46 |
+
dupb.append(q)
|
| 47 |
+
else:
|
| 48 |
+
dupa.append(p)
|
| 49 |
+
resa = [t for i, t in enumerate(stracksa) if not i in dupa]
|
| 50 |
+
resb = [t for i, t in enumerate(stracksb) if not i in dupb]
|
| 51 |
+
return resa, resb
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class Tracklet(BaseTrack):
|
| 55 |
+
shared_kalman = KalmanFilter()
|
| 56 |
+
|
| 57 |
+
def __init__(self, tlwh, score, cls, angle=None,
|
| 58 |
+
feat=None, feat_history=50,
|
| 59 |
+
enable_buffer=True, obj_img=None):
|
| 60 |
+
self._tlwh = np.asarray(tlwh, dtype=np.float32)
|
| 61 |
+
# wait activate
|
| 62 |
+
self.score = score
|
| 63 |
+
self.cls = -1
|
| 64 |
+
|
| 65 |
+
self.kalman_filter = None
|
| 66 |
+
self.mean, self.covariance = None, None
|
| 67 |
+
# add trajectory, last observation, last_mean,last_cov
|
| 68 |
+
self.trajectory = None # buffer to keep track of trajectory
|
| 69 |
+
# last observation before lost (frame_id,det_bbox)
|
| 70 |
+
self.last_observation = None
|
| 71 |
+
# last mean,last observation before lost
|
| 72 |
+
self.last_mean, self.last_covariance = None, None
|
| 73 |
+
self.last_frame_id = None # last frame_id before lost
|
| 74 |
+
|
| 75 |
+
self.is_activated = False
|
| 76 |
+
|
| 77 |
+
self.cls_hist = [] # (cls id, freq)
|
| 78 |
+
self.update_cls(cls, score)
|
| 79 |
+
|
| 80 |
+
self.tracklet_len = 0
|
| 81 |
+
|
| 82 |
+
self.angle = angle
|
| 83 |
+
|
| 84 |
+
# reid
|
| 85 |
+
norm_feat = self.norm_feat(feat) if feat is not None else None
|
| 86 |
+
self.smooth_feat = norm_feat
|
| 87 |
+
self.curr_feat = norm_feat
|
| 88 |
+
self.feat_momentum = 0.9
|
| 89 |
+
self.obj_img = obj_img
|
| 90 |
+
self.best_det_feat = norm_feat
|
| 91 |
+
self.best_obj_img = obj_img
|
| 92 |
+
|
| 93 |
+
# buffer of feature
|
| 94 |
+
self.enable_buffer = enable_buffer
|
| 95 |
+
self.feat_buffer = list()
|
| 96 |
+
self.box_buffer = list() # tlwh
|
| 97 |
+
self.obj_img_buffer = list()
|
| 98 |
+
|
| 99 |
+
self.lost_frame_num = 0
|
| 100 |
+
|
| 101 |
+
self.history_info = []
|
| 102 |
+
|
| 103 |
+
self.active_frames = []
|
| 104 |
+
|
| 105 |
+
def update_active_frames(self, frame):
|
| 106 |
+
if not isinstance(frame, list):
|
| 107 |
+
self.active_frames.append(frame)
|
| 108 |
+
else:
|
| 109 |
+
self.active_frames.extend(frame)
|
| 110 |
+
|
| 111 |
+
def set_lost_frame_num(self):
|
| 112 |
+
self.lost_frame_num += 1
|
| 113 |
+
|
| 114 |
+
def reset_lost_frame_num(self):
|
| 115 |
+
self.lost_frame_num = 0
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def norm_feat(feat):
|
| 119 |
+
feat /= np.linalg.norm(feat)
|
| 120 |
+
return feat
|
| 121 |
+
|
| 122 |
+
def buffer_areas(self):
|
| 123 |
+
buf_areas = np.array([_bbox[2]*_bbox[3] for _bbox in self.box_buffer])
|
| 124 |
+
return buf_areas
|
| 125 |
+
|
| 126 |
+
def update_reid_buffer(self, new_track):
|
| 127 |
+
N = 15
|
| 128 |
+
# check wheather the obj image size and ratio is different from current image sizes
|
| 129 |
+
# --> update the buffer if the condition is True
|
| 130 |
+
update = False
|
| 131 |
+
if len(self.box_buffer) == 0:
|
| 132 |
+
update = True
|
| 133 |
+
else:
|
| 134 |
+
new_tlwh = new_track._tlwh
|
| 135 |
+
# calculate the area of boxes
|
| 136 |
+
buf_areas = self.buffer_areas()
|
| 137 |
+
new_area = new_tlwh[2] * new_tlwh[3]
|
| 138 |
+
|
| 139 |
+
# Calculate the percentage value
|
| 140 |
+
percentage = N / 100
|
| 141 |
+
|
| 142 |
+
# Check if new_area is N% bigger or smaller than every box in buf_areas
|
| 143 |
+
is_bigger = np.all(new_area >= (1 + percentage) * buf_areas)
|
| 144 |
+
is_smaller = np.all(new_area <= (1 - percentage) * buf_areas)
|
| 145 |
+
|
| 146 |
+
if is_bigger or is_smaller:
|
| 147 |
+
update = True
|
| 148 |
+
|
| 149 |
+
if update:
|
| 150 |
+
self.feat_buffer.append(new_track.curr_feat)
|
| 151 |
+
self.box_buffer.append(new_track._tlwh)
|
| 152 |
+
self.obj_img_buffer.append(new_track.obj_img)
|
| 153 |
+
|
| 154 |
+
def best_area_feat(self, new_box):
|
| 155 |
+
# new_box: tlwh
|
| 156 |
+
# return a best match feature and obj image base on min area
|
| 157 |
+
if len(self.feat_buffer) == 0 or not self.enable_buffer:
|
| 158 |
+
return dict(
|
| 159 |
+
feat=self.curr_feat,
|
| 160 |
+
obj_img=self.obj_img)
|
| 161 |
+
|
| 162 |
+
new_box_area = new_box[2] * new_box[3]
|
| 163 |
+
buf_areas = self.buffer_areas()
|
| 164 |
+
dif = np.abs(buf_areas - new_box_area)
|
| 165 |
+
|
| 166 |
+
best_match_idx = np.argmin(dif)
|
| 167 |
+
|
| 168 |
+
best_match_results = dict(
|
| 169 |
+
feat=self.feat_buffer[best_match_idx],
|
| 170 |
+
obj_img=self.obj_img_buffer[best_match_idx]
|
| 171 |
+
)
|
| 172 |
+
return best_match_results
|
| 173 |
+
|
| 174 |
+
def update_features(self, new_track):
|
| 175 |
+
feat = new_track.curr_feat
|
| 176 |
+
self.smooth_feat = (self.feat_momentum * self.smooth_feat
|
| 177 |
+
+ (1 - self.feat_momentum) * feat)
|
| 178 |
+
self.smooth_feat = self.norm_feat(self.smooth_feat)
|
| 179 |
+
|
| 180 |
+
if self.enable_buffer:
|
| 181 |
+
# update feature buffer based on boxsize
|
| 182 |
+
self.update_reid_buffer(new_track)
|
| 183 |
+
|
| 184 |
+
# update current feature
|
| 185 |
+
self.curr_feat = feat
|
| 186 |
+
self.obj_img = new_track.obj_img
|
| 187 |
+
|
| 188 |
+
# update best reid feature base on detection score
|
| 189 |
+
if new_track.score >= self.score:
|
| 190 |
+
self.best_det_feat = feat
|
| 191 |
+
self.best_obj_img = new_track.obj_img
|
| 192 |
+
|
| 193 |
+
def update_cls(self, cls, score):
|
| 194 |
+
if len(self.cls_hist) > 0:
|
| 195 |
+
max_freq = 0
|
| 196 |
+
found = False
|
| 197 |
+
for c in self.cls_hist:
|
| 198 |
+
if cls == c[0]:
|
| 199 |
+
c[1] += score
|
| 200 |
+
found = True
|
| 201 |
+
|
| 202 |
+
if c[1] > max_freq:
|
| 203 |
+
max_freq = c[1]
|
| 204 |
+
self.cls = c[0]
|
| 205 |
+
if not found:
|
| 206 |
+
self.cls_hist.append([cls, score])
|
| 207 |
+
self.cls = cls
|
| 208 |
+
else:
|
| 209 |
+
self.cls_hist.append([cls, score])
|
| 210 |
+
self.cls = cls
|
| 211 |
+
|
| 212 |
+
def update_angle(self, angle, score):
|
| 213 |
+
self.angle = (1-score)*self.angle + score*angle
|
| 214 |
+
|
| 215 |
+
@staticmethod
|
| 216 |
+
def multi_predict(stracks):
|
| 217 |
+
if len(stracks) > 0:
|
| 218 |
+
multi_mean = np.asarray([st.mean.copy() for st in stracks])
|
| 219 |
+
multi_covariance = np.asarray([st.covariance for st in stracks])
|
| 220 |
+
for i, st in enumerate(stracks):
|
| 221 |
+
if st.state != TrackState.Tracked:
|
| 222 |
+
multi_mean[i][6] = 0
|
| 223 |
+
multi_mean[i][7] = 0
|
| 224 |
+
multi_mean, multi_covariance = Tracklet.shared_kalman.multi_predict(
|
| 225 |
+
multi_mean, multi_covariance)
|
| 226 |
+
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
|
| 227 |
+
stracks[i].mean = mean
|
| 228 |
+
stracks[i].covariance = cov
|
| 229 |
+
|
| 230 |
+
@staticmethod
|
| 231 |
+
def multi_gmc(stracks, Hmat=np.eye(3, 3)):
|
| 232 |
+
# This approach uses Homography matrix
|
| 233 |
+
if len(stracks) > 0:
|
| 234 |
+
multi_mean = [st.mean.reshape(-1, 1, 2) for st in stracks]
|
| 235 |
+
multi_covariance = np.asarray([st.covariance for st in stracks])
|
| 236 |
+
|
| 237 |
+
R, T = Hmat[:2, :2], Hmat[:2, 2]
|
| 238 |
+
R8x8 = np.kron(np.eye(4, dtype=float), R)
|
| 239 |
+
H = deepcopy(Hmat)
|
| 240 |
+
H[:2, 2] = 0 # remove translation
|
| 241 |
+
|
| 242 |
+
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
|
| 243 |
+
# w = mean.reshape(4,2).dot(V)
|
| 244 |
+
# R_ = [R/w_i for w_i in w]
|
| 245 |
+
# R8x8 = block_diag(*R_)
|
| 246 |
+
mean = cv2.perspectiveTransform(mean, H)
|
| 247 |
+
mean = mean.reshape(-1)
|
| 248 |
+
mean[:2] += T
|
| 249 |
+
cov = R8x8.dot(cov).dot(R8x8.transpose())
|
| 250 |
+
|
| 251 |
+
stracks[i].mean = mean
|
| 252 |
+
stracks[i].covariance = cov
|
| 253 |
+
|
| 254 |
+
last_obs, last_frame_id = stracks[i].last_observation, stracks[i].last_frame_id
|
| 255 |
+
last_mean, last_cov = stracks[i].last_mean, stracks[i].last_covariance
|
| 256 |
+
|
| 257 |
+
if last_mean is not None:
|
| 258 |
+
# Update the last observation
|
| 259 |
+
last_obs = cv2.perspectiveTransform(
|
| 260 |
+
last_obs.reshape(-1, 1, 2), H)
|
| 261 |
+
last_obs = last_obs.reshape(-1)
|
| 262 |
+
last_obs[:2] += T
|
| 263 |
+
last_mean = cv2.perspectiveTransform(
|
| 264 |
+
last_mean.reshape(-1, 1, 2), H)
|
| 265 |
+
last_mean = last_mean.reshape(-1)
|
| 266 |
+
last_mean[:2] += T
|
| 267 |
+
last_cov = R8x8.dot(last_cov).dot(R8x8.transpose())
|
| 268 |
+
stracks[i].last_observation = last_obs
|
| 269 |
+
stracks[i].last_mean = last_mean
|
| 270 |
+
stracks[i].last_covariance = last_cov
|
| 271 |
+
# save the trajectory
|
| 272 |
+
traj = last_mean[:4]
|
| 273 |
+
stracks[i].trajectory = np.append(traj, last_frame_id)
|
| 274 |
+
|
| 275 |
+
def activate(self, kalman_filter, frame_id):
|
| 276 |
+
"""Start a new tracklet"""
|
| 277 |
+
self.kalman_filter = kalman_filter
|
| 278 |
+
self.track_id = self.next_id()
|
| 279 |
+
|
| 280 |
+
xywh = self.tlwh_to_xywh(self._tlwh)
|
| 281 |
+
self.mean, self.covariance = self.kalman_filter.initiate(xywh)
|
| 282 |
+
|
| 283 |
+
self.tracklet_len = 0
|
| 284 |
+
self.state = TrackState.Tracked
|
| 285 |
+
if frame_id == 1:
|
| 286 |
+
self.is_activated = True
|
| 287 |
+
self.frame_id = frame_id
|
| 288 |
+
self.start_frame = frame_id
|
| 289 |
+
|
| 290 |
+
# Save trajectory
|
| 291 |
+
self.last_frame_id = frame_id
|
| 292 |
+
self.last_observation = deepcopy(xywh)
|
| 293 |
+
self.last_mean = deepcopy(self.mean)
|
| 294 |
+
self.last_covariance = deepcopy(self.covariance)
|
| 295 |
+
|
| 296 |
+
self.set_history_info(
|
| 297 |
+
history_info=[dict(frame_id=frame_id, track_id=self.track_id), ],
|
| 298 |
+
append_first=False)
|
| 299 |
+
|
| 300 |
+
self.update_active_frames(frame_id)
|
| 301 |
+
|
| 302 |
+
def re_activate(self, new_track, frame_id, new_id=False):
|
| 303 |
+
xywh = self.tlwh_to_xywh(new_track.tlwh)
|
| 304 |
+
self.mean, self.covariance = self.kalman_filter.update(self.mean,
|
| 305 |
+
self.covariance, xywh,
|
| 306 |
+
new_track.score)
|
| 307 |
+
if new_track.curr_feat is not None:
|
| 308 |
+
self.update_features(new_track)
|
| 309 |
+
self.tracklet_len = 0
|
| 310 |
+
self.state = TrackState.Tracked
|
| 311 |
+
self.is_activated = True
|
| 312 |
+
self.frame_id = frame_id
|
| 313 |
+
if new_id:
|
| 314 |
+
self.track_id = self.next_id()
|
| 315 |
+
self.score = new_track.score
|
| 316 |
+
|
| 317 |
+
self.update_cls(new_track.cls, new_track.score)
|
| 318 |
+
if self.angle is not None:
|
| 319 |
+
self.update_angle(new_track.angle, new_track.score)
|
| 320 |
+
|
| 321 |
+
# Save trajectory
|
| 322 |
+
self.last_frame_id = frame_id
|
| 323 |
+
self.last_observation = deepcopy(xywh)
|
| 324 |
+
self.last_mean = deepcopy(self.mean)
|
| 325 |
+
self.last_covariance = deepcopy(self.covariance)
|
| 326 |
+
self.set_history_info(
|
| 327 |
+
history_info=[dict(frame_id=frame_id, track_id=self.track_id), ],
|
| 328 |
+
append_first=False)
|
| 329 |
+
self.update_active_frames(frame_id)
|
| 330 |
+
|
| 331 |
+
def update(self, new_track, frame_id):
|
| 332 |
+
"""
|
| 333 |
+
Update a matched track
|
| 334 |
+
:type new_track: Tracklet
|
| 335 |
+
:type frame_id: int
|
| 336 |
+
:type update_feature: bool
|
| 337 |
+
:return:
|
| 338 |
+
"""
|
| 339 |
+
self.frame_id = frame_id
|
| 340 |
+
self.tracklet_len += 1
|
| 341 |
+
|
| 342 |
+
new_xywh = self.tlwh_to_xywh(new_track.tlwh)
|
| 343 |
+
self.mean, self.covariance = self.kalman_filter.update(
|
| 344 |
+
self.mean, self.covariance, new_xywh, new_track.score)
|
| 345 |
+
|
| 346 |
+
# update reid feature
|
| 347 |
+
if new_track.curr_feat is not None:
|
| 348 |
+
self.update_features(new_track)
|
| 349 |
+
|
| 350 |
+
self.state = TrackState.Tracked
|
| 351 |
+
self.is_activated = True
|
| 352 |
+
|
| 353 |
+
self.score = new_track.score
|
| 354 |
+
self.update_cls(new_track.cls, new_track.score)
|
| 355 |
+
|
| 356 |
+
if self.angle is not None:
|
| 357 |
+
self.update_angle(new_track.angle, new_track.score)
|
| 358 |
+
|
| 359 |
+
# Save trajectory
|
| 360 |
+
self.last_frame_id = frame_id
|
| 361 |
+
self.last_observation = deepcopy(new_xywh)
|
| 362 |
+
self.last_mean = deepcopy(self.mean)
|
| 363 |
+
self.last_covariance = deepcopy(self.covariance)
|
| 364 |
+
if self.is_activated:
|
| 365 |
+
self.set_history_info(
|
| 366 |
+
history_info=[
|
| 367 |
+
dict(frame_id=frame_id, track_id=self.track_id), ],
|
| 368 |
+
append_first=False)
|
| 369 |
+
self.update_active_frames(frame_id)
|
| 370 |
+
|
| 371 |
+
def smooth_update(self, new_track, frame_id):
|
| 372 |
+
'''
|
| 373 |
+
As introduced in OC-SORT
|
| 374 |
+
'''
|
| 375 |
+
xywh = self.tlwh_to_xywh(new_track.tlwh)
|
| 376 |
+
# Interpolate update
|
| 377 |
+
if self.last_frame_id is not None:
|
| 378 |
+
num_missing_steps = frame_id - self.last_frame_id
|
| 379 |
+
if num_missing_steps > 1:
|
| 380 |
+
delta = (xywh - self.last_observation)/num_missing_steps
|
| 381 |
+
interpolate_tracks = [self.last_observation +
|
| 382 |
+
i*delta for i in range(1, num_missing_steps)]
|
| 383 |
+
mean_i, covariance_i = self.last_mean, self.last_covariance
|
| 384 |
+
for new_xywh in interpolate_tracks:
|
| 385 |
+
# Predict
|
| 386 |
+
mean_i, covariance_i = self.kalman_filter.predict(
|
| 387 |
+
mean_i, covariance_i)
|
| 388 |
+
# Update
|
| 389 |
+
mean_i, covariance_i = self.kalman_filter.update(
|
| 390 |
+
mean_i, covariance_i, new_xywh, new_track.score)
|
| 391 |
+
# the prediction step before last update
|
| 392 |
+
self.mean, self.covariance = self.kalman_filter.predict(
|
| 393 |
+
mean_i, covariance_i)
|
| 394 |
+
|
| 395 |
+
# Normal update
|
| 396 |
+
self.update(new_track, frame_id)
|
| 397 |
+
|
| 398 |
+
@property
|
| 399 |
+
def velocity(self):
|
| 400 |
+
vx = self.mean[4] # horizontal velocity
|
| 401 |
+
vy = self.mean[5] # vertical velocity
|
| 402 |
+
vh = self.mean[7] # height velocity
|
| 403 |
+
return [vx, vy, vh]
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def vel_dir(self):
|
| 407 |
+
if self.trajectory is None:
|
| 408 |
+
dist = self.mean[4:6] # velocity
|
| 409 |
+
else:
|
| 410 |
+
p2 = self.mean[:2]
|
| 411 |
+
p1 = self.trajectory[:2] # (x,y,w,h,frame_id)
|
| 412 |
+
dist = p2-p1
|
| 413 |
+
norm = np.linalg.norm(dist) + 1e-6
|
| 414 |
+
return dist/norm
|
| 415 |
+
|
| 416 |
+
def common_active_frames(self, track):
|
| 417 |
+
lst3 = [value for value in self.active_frames if value in track.active_frames]
|
| 418 |
+
return len(lst3) > 0
|
| 419 |
+
|
| 420 |
+
def set_history_info(self, history_info, append_first=False):
|
| 421 |
+
if append_first:
|
| 422 |
+
self.history_info = history_info + self.history_info
|
| 423 |
+
else:
|
| 424 |
+
self.history_info = self.history_info + history_info
|
| 425 |
+
|
| 426 |
+
def get_history_info(self):
|
| 427 |
+
return self.history_info
|
models/models/trackers/reid_parallel_tracker/matchers/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .distances import DistSOLIDER
|
| 2 |
+
from .base_matchers import SimMatcher
|
| 3 |
+
from .single_stage_matcher import SingleStageMatcher
|
| 4 |
+
from .prioritize_reid_matcher import PrioritizeReidMatcher
|
models/models/trackers/reid_parallel_tracker/matchers/base_matchers.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import List, Tuple, Dict
|
| 3 |
+
from .distances import DistCosine
|
| 4 |
+
from ..core.matching import linear_assignment, topk_assignment
|
| 5 |
+
from ..core.tracklet import Tracklet
|
| 6 |
+
|
| 7 |
+
class SimMatcher():
|
| 8 |
+
"""The baseline matcher that only use linear_assignment to match tracklets with detection boxes
|
| 9 |
+
"""
|
| 10 |
+
def __init__(self,
|
| 11 |
+
dist_cfg: Dict,
|
| 12 |
+
match_thr: float):
|
| 13 |
+
self.dist = DistCosine(**dist_cfg)
|
| 14 |
+
self.match_thr = match_thr
|
| 15 |
+
|
| 16 |
+
def __call__(self,
|
| 17 |
+
tracks: List[Tracklet],
|
| 18 |
+
dets: List[Tracklet]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 19 |
+
""" Associate with tracklets with detection boxes"""
|
| 20 |
+
dists = self.dist(tracks, dets)
|
| 21 |
+
matches_idx, unmatched_tracks_idx, unmatched_dets_idx = linear_assignment(dists, thresh=self.match_thr)
|
| 22 |
+
# matches_idx, unmatched_tracks_idx, unmatched_dets_idx = topk_assignment(dists, thresh=self.match_thr, topk=2)
|
| 23 |
+
return matches_idx, unmatched_tracks_idx, unmatched_dets_idx
|
| 24 |
+
|
| 25 |
+
def matching_dists(self, tracks: List[Tracklet],
|
| 26 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 27 |
+
""" Compute the distance between tracklets and detections"""
|
| 28 |
+
return self.dist(tracks, dets)
|
| 29 |
+
|
| 30 |
+
def matching_scores(self, tracks: List[Tracklet],
|
| 31 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 32 |
+
""" Compute the matching scores between tracklets and detections"""
|
| 33 |
+
return self.dist.matching_scores(tracks, dets)
|
| 34 |
+
|
| 35 |
+
def assign(self, distances: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 36 |
+
return linear_assignment(distances, thresh=self.match_thr)
|
| 37 |
+
|
| 38 |
+
def __repr__(self):
|
| 39 |
+
return f"SimMatcher(dist={self.dist}, match_thr={self.match_thr})"
|
models/models/trackers/reid_parallel_tracker/matchers/distances.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
from ..core.tracklet import Tracklet
|
| 3 |
+
from ..core.matching import (embedding_cosine_distance,
|
| 4 |
+
euclidean_distance_matrix, solider_distance,
|
| 5 |
+
best_area_solider_distance)
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
class BaseDist():
|
| 9 |
+
def __init__(self,
|
| 10 |
+
weight=None,
|
| 11 |
+
weight_type="dot"):
|
| 12 |
+
self.weight = weight
|
| 13 |
+
self.weight_type = weight_type.lower()
|
| 14 |
+
assert self.weight_type in ["dot","power"], "only support weight type of dot(multiply w*dist), power (dist^w)"
|
| 15 |
+
|
| 16 |
+
def weighted_dist(self, dists):
|
| 17 |
+
if self.weight is not None:
|
| 18 |
+
dists = self.weight*dists if self.weight_type == "dot" else np.power(dists,self.weight)
|
| 19 |
+
return dists
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DistCosine(BaseDist):
|
| 23 |
+
# ReID distance
|
| 24 |
+
def __init__(self,
|
| 25 |
+
location_aware=dict(search_radius_scale=10),
|
| 26 |
+
feature_type='curr_feat',
|
| 27 |
+
norm=False,
|
| 28 |
+
**kwargs):
|
| 29 |
+
"""
|
| 30 |
+
- use_location_aware: each object will only be matched with nearby objects
|
| 31 |
+
which is inside a circle with R = Width * SRS
|
| 32 |
+
"""
|
| 33 |
+
self.location_aware = location_aware
|
| 34 |
+
self.feature_type = feature_type
|
| 35 |
+
self.norm = norm
|
| 36 |
+
self.use_adaptive_search_radius = ('early_track_lost_frame' in self.location_aware) \
|
| 37 |
+
and ('early_lost_track_search_radius_scale' in self.location_aware) \
|
| 38 |
+
and ('active_track_search_radius_scale' in self.location_aware)
|
| 39 |
+
assert self.feature_type in ['curr_feat', 'smooth_feat', 'best_det_feat']
|
| 40 |
+
super().__init__(**kwargs)
|
| 41 |
+
|
| 42 |
+
def __call__(self,
|
| 43 |
+
tracklets: List[Tracklet],
|
| 44 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 45 |
+
# calculate distance base on embedding
|
| 46 |
+
dists = embedding_cosine_distance(tracklets, dets,
|
| 47 |
+
self.feature_type,
|
| 48 |
+
norm=self.norm)
|
| 49 |
+
# if use localtion aware
|
| 50 |
+
if len(self.location_aware) and len(tracklets):
|
| 51 |
+
distance_results = euclidean_distance_matrix(tracklets, dets)
|
| 52 |
+
distance_matrix = distance_results['distance_matrix']
|
| 53 |
+
width_atracks = distance_results['width_atracks']
|
| 54 |
+
|
| 55 |
+
if not self.use_adaptive_search_radius:
|
| 56 |
+
# fixed searching radius scale
|
| 57 |
+
invalid_reid_search_regions = distance_matrix >= width_atracks*self.location_aware['search_radius_scale']
|
| 58 |
+
else:
|
| 59 |
+
# adaptive search radius scale
|
| 60 |
+
lost_frame_nums = np.array([_track.lost_frame_num for _track in tracklets])
|
| 61 |
+
is_active = lost_frame_nums == 0
|
| 62 |
+
is_early_lost_track = (lost_frame_nums <= self.location_aware['early_track_lost_frame']) * (lost_frame_nums > 0)
|
| 63 |
+
is_long_lost_track = lost_frame_nums > self.location_aware['early_track_lost_frame']
|
| 64 |
+
|
| 65 |
+
lost_frame_nums[is_active] = self.location_aware['active_track_search_radius_scale']
|
| 66 |
+
lost_frame_nums[is_early_lost_track] = self.location_aware['early_lost_track_search_radius_scale']
|
| 67 |
+
lost_frame_nums[is_long_lost_track] = self.location_aware['long_lost_track_search_radius_scale']
|
| 68 |
+
lost_frame_nums = lost_frame_nums.reshape(-1, 1)
|
| 69 |
+
invalid_reid_search_regions = distance_matrix >= width_atracks*lost_frame_nums
|
| 70 |
+
# update distance threshold
|
| 71 |
+
dists[invalid_reid_search_regions] = 1e4
|
| 72 |
+
|
| 73 |
+
return self.weighted_dist(dists)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def matching_scores(self,
|
| 77 |
+
tracklets: List[Tracklet],
|
| 78 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 79 |
+
""" convienient function to return the matching scores instead of the distance"""
|
| 80 |
+
dists = embedding_cosine_distance(tracklets, dets,
|
| 81 |
+
self.feature_type,
|
| 82 |
+
norm=self.norm)
|
| 83 |
+
matching_scores = 1 - dists
|
| 84 |
+
return matching_scores
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class DistSOLIDER(BaseDist):
|
| 88 |
+
# ReID distance
|
| 89 |
+
def __init__(self,
|
| 90 |
+
location_aware=dict(search_radius_scale=10),
|
| 91 |
+
feature_type='curr_feat',
|
| 92 |
+
norm=False,
|
| 93 |
+
**kwargs):
|
| 94 |
+
"""
|
| 95 |
+
- use_location_aware: each object will only be matched with nearby objects
|
| 96 |
+
which is inside a circle with R=Width * search_radius_scale
|
| 97 |
+
"""
|
| 98 |
+
self.location_aware = location_aware
|
| 99 |
+
self.feature_type = feature_type
|
| 100 |
+
self.norm = norm
|
| 101 |
+
assert self.feature_type in ['curr_feat', 'smooth_feat', 'best_area_feat', 'best_det_feat']
|
| 102 |
+
super().__init__(**kwargs)
|
| 103 |
+
|
| 104 |
+
def __call__(self,
|
| 105 |
+
tracklets: List[Tracklet],
|
| 106 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 107 |
+
# calculate distance base on embedding
|
| 108 |
+
if self.feature_type == "best_area_feat":
|
| 109 |
+
dists = best_area_solider_distance(tracklets, dets, self.norm)
|
| 110 |
+
else:
|
| 111 |
+
dists = solider_distance(tracklets, dets,
|
| 112 |
+
self.feature_type,
|
| 113 |
+
norm=self.norm)
|
| 114 |
+
# if use localtion aware
|
| 115 |
+
if self.location_aware is not None:
|
| 116 |
+
distance_results = euclidean_distance_matrix(tracklets, dets)
|
| 117 |
+
distance_matrix = distance_results['distance_matrix']
|
| 118 |
+
width_atracks = distance_results['width_atracks']
|
| 119 |
+
invalid_reid_search_regions = distance_matrix >= width_atracks*self.location_aware['search_radius_scale']
|
| 120 |
+
try:
|
| 121 |
+
dists[invalid_reid_search_regions] = 1.0
|
| 122 |
+
except:
|
| 123 |
+
import ipdb; ipdb.set_trace()
|
| 124 |
+
return self.weighted_dist(dists)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def matching_scores(self,
|
| 128 |
+
tracklets: List[Tracklet],
|
| 129 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 130 |
+
""" convienient function to return the matching scores instead of the distance"""
|
| 131 |
+
NotImplementedError('Not implemented')
|
| 132 |
+
|
models/models/trackers/reid_parallel_tracker/matchers/prioritize_reid_matcher.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from copy import deepcopy
|
| 3 |
+
from typing import List, Tuple, Dict
|
| 4 |
+
from ..core.matching import linear_assignment, topk_assignment
|
| 5 |
+
from ..core.tracklet import Tracklet
|
| 6 |
+
from ..core.tracklet import TrackState
|
| 7 |
+
from .base_matchers import SimMatcher
|
| 8 |
+
from .distances import DistCosine
|
| 9 |
+
|
| 10 |
+
class PrioritizeReidMatcher():
|
| 11 |
+
def __init__(self,
|
| 12 |
+
reid_distance,
|
| 13 |
+
iou_distance,
|
| 14 |
+
match_with_reid_thr,
|
| 15 |
+
islost_match_thr,
|
| 16 |
+
isactive_match_thr):
|
| 17 |
+
|
| 18 |
+
self.reid_dist = DistCosine(**reid_distance)
|
| 19 |
+
self.iou_dist = DistCosine(**iou_distance)
|
| 20 |
+
self.match_with_reid_thr = match_with_reid_thr
|
| 21 |
+
self.islost_match_thr = islost_match_thr
|
| 22 |
+
self.isactive_match_thr = isactive_match_thr
|
| 23 |
+
|
| 24 |
+
def __call__(self,
|
| 25 |
+
tracks: List[Tracklet],
|
| 26 |
+
dets: List[Tracklet]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 27 |
+
""" Associate with tracklets with detection boxes"""
|
| 28 |
+
|
| 29 |
+
# 1. Match with ReID dist <0.15
|
| 30 |
+
dist_reid = self.reid_dist(tracks, dets)
|
| 31 |
+
matches_idx_1, unmatched_tracks_idx_1, unmatched_dets_idx_1 = self.assign(dist_reid,
|
| 32 |
+
thresh=self.match_with_reid_thr)
|
| 33 |
+
|
| 34 |
+
# split unmatch track, det
|
| 35 |
+
unmatched_tracks = [tracks[idx] for idx in unmatched_tracks_idx_1]
|
| 36 |
+
unmatched_dets = [dets[i] for i in unmatched_dets_idx_1]
|
| 37 |
+
|
| 38 |
+
# split active and lost tracks
|
| 39 |
+
unmatched_lost_tracks = []
|
| 40 |
+
unmatched_lost_tracks_idx = []
|
| 41 |
+
unmatched_active_tracks = []
|
| 42 |
+
unmatched_active_tracks_idx = []
|
| 43 |
+
for idx, _track in enumerate(unmatched_tracks):
|
| 44 |
+
if _track.state == TrackState.Lost:
|
| 45 |
+
unmatched_lost_tracks.append(_track)
|
| 46 |
+
unmatched_lost_tracks_idx.append(unmatched_tracks_idx_1[idx])
|
| 47 |
+
else:
|
| 48 |
+
unmatched_active_tracks.append(_track)
|
| 49 |
+
unmatched_active_tracks_idx.append(unmatched_tracks_idx_1[idx])
|
| 50 |
+
|
| 51 |
+
# 2. is lost; lost object expect lower IoU threshold lower ReID
|
| 52 |
+
matches_idx_2, unmatched_tracks_idx_2, unmatched_dets_idx_2 = self.reid_and_iou_matching(
|
| 53 |
+
unmatched_lost_tracks,
|
| 54 |
+
unmatched_lost_tracks_idx,
|
| 55 |
+
unmatched_dets,
|
| 56 |
+
unmatched_dets_idx_1,
|
| 57 |
+
self.islost_match_thr)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# 3. Active object expect higher IoU threshold
|
| 61 |
+
# split remain detection
|
| 62 |
+
from2_unmatched_dets = [dets[i] for i in unmatched_dets_idx_2]
|
| 63 |
+
from2_unmatched_dets_idx = unmatched_dets_idx_2
|
| 64 |
+
matches_idx_3, unmatched_tracks_idx_3, unmatched_dets_idx_3 = self.reid_and_iou_matching(
|
| 65 |
+
unmatched_active_tracks,
|
| 66 |
+
unmatched_active_tracks_idx,
|
| 67 |
+
from2_unmatched_dets,
|
| 68 |
+
from2_unmatched_dets_idx,
|
| 69 |
+
self.isactive_match_thr)
|
| 70 |
+
|
| 71 |
+
# merge result
|
| 72 |
+
matches_idx = matches_idx_1.tolist() + matches_idx_2.tolist() + matches_idx_3.tolist()
|
| 73 |
+
unmatched_tracks_idx = []
|
| 74 |
+
unmatched_dets_idx = []
|
| 75 |
+
|
| 76 |
+
if len(matches_idx):
|
| 77 |
+
matches_idx = np.array(matches_idx)
|
| 78 |
+
else:
|
| 79 |
+
matches_idx = np.empty((0, 2), dtype=np.int64)
|
| 80 |
+
|
| 81 |
+
# update unmatch tracks
|
| 82 |
+
for idx in range(len(tracks)):
|
| 83 |
+
if not idx in matches_idx[:, 0]:
|
| 84 |
+
unmatched_tracks_idx.append(idx)
|
| 85 |
+
unmatched_tracks_idx = np.array(unmatched_tracks_idx)
|
| 86 |
+
# update unmatch dets
|
| 87 |
+
for idx in range(len(dets)):
|
| 88 |
+
if not idx in matches_idx[:, 1]:
|
| 89 |
+
unmatched_dets_idx.append(idx)
|
| 90 |
+
unmatched_dets_idx = np.array(unmatched_dets_idx)
|
| 91 |
+
|
| 92 |
+
return matches_idx, unmatched_tracks_idx, unmatched_dets_idx
|
| 93 |
+
|
| 94 |
+
def calculate_occluded_ratio(self, dets):
|
| 95 |
+
det_ious = 1 - self.iou_dist(dets, dets)
|
| 96 |
+
np.fill_diagonal(det_ious, 0)
|
| 97 |
+
det_scores = np.array([det.score for det in dets])
|
| 98 |
+
det_score_matrix = (det_scores[:, None] < det_scores[None, :]).astype(np.float32)
|
| 99 |
+
occluded_ratio_matrix = det_ious * det_score_matrix
|
| 100 |
+
# TODO: Take area --> done, same performance.
|
| 101 |
+
if len(occluded_ratio_matrix):
|
| 102 |
+
occluded_ratio = np.max(occluded_ratio_matrix, 1)
|
| 103 |
+
return occluded_ratio
|
| 104 |
+
else:
|
| 105 |
+
return []
|
| 106 |
+
|
| 107 |
+
def reid_and_iou_matching(self,
|
| 108 |
+
unmatched_tracks,
|
| 109 |
+
unmatched_tracks_idx,
|
| 110 |
+
unmatched_dets,
|
| 111 |
+
unmatched_dets_idx,
|
| 112 |
+
match_conditions):
|
| 113 |
+
# 2. is lost; lost object expect lower IoU threshold lower ReID
|
| 114 |
+
lost_tracks_reid_dist = self.reid_dist(unmatched_tracks, unmatched_dets)
|
| 115 |
+
lost_tracks_iou_dist = self.iou_dist(unmatched_tracks, unmatched_dets)
|
| 116 |
+
|
| 117 |
+
# reweight reid
|
| 118 |
+
occluded_ratio = self.calculate_occluded_ratio(unmatched_dets)
|
| 119 |
+
|
| 120 |
+
# if len(occluded_ratio):
|
| 121 |
+
### reid_score *= e^(-occluded_ratio)
|
| 122 |
+
# reid_reweighting = np.exp(-occluded_ratio)
|
| 123 |
+
# lost_tracks_reid_dist = lost_tracks_reid_dist * reid_reweighting
|
| 124 |
+
|
| 125 |
+
### (x^2)/3
|
| 126 |
+
# lost_tracks_reid_dist = lost_tracks_reid_dist + (occluded_ratio**2)/3
|
| 127 |
+
|
| 128 |
+
occluded_thr = 0.3
|
| 129 |
+
used_occluded = False
|
| 130 |
+
|
| 131 |
+
# cascade reid matching
|
| 132 |
+
if len(occluded_ratio):
|
| 133 |
+
# if occluded: Match non occluded object first, then match occluded one
|
| 134 |
+
is_occluded_idx = np.nonzero(occluded_ratio > occluded_thr)[0]
|
| 135 |
+
isnot_occluded_idx = np.nonzero(occluded_ratio <= occluded_thr)[0]
|
| 136 |
+
if len(is_occluded_idx) and len(unmatched_tracks):
|
| 137 |
+
used_occluded = True
|
| 138 |
+
|
| 139 |
+
# matching visible det objects
|
| 140 |
+
vis_lost_tracks_reid_dist = deepcopy(lost_tracks_reid_dist)
|
| 141 |
+
vis_lost_tracks_reid_dist[:, is_occluded_idx] = 1e4
|
| 142 |
+
vis_matched_idx, vis_unmatched_tracks_idx, vis_unmatched_dets_idx = self.assign(vis_lost_tracks_reid_dist,
|
| 143 |
+
thresh=match_conditions['reid_thr'])
|
| 144 |
+
# matching occluded det objects
|
| 145 |
+
occluded_lost_tracks_reid_dist = deepcopy(lost_tracks_reid_dist)
|
| 146 |
+
# if tracklet already match --> ignore
|
| 147 |
+
occluded_lost_tracks_reid_dist[vis_matched_idx[:, 0], :] = 1e4
|
| 148 |
+
# update visible positions = 1e4
|
| 149 |
+
occluded_lost_tracks_reid_dist[:, isnot_occluded_idx] = 1e4
|
| 150 |
+
# matching
|
| 151 |
+
occ_matched_idx, occ_unmatched_tracks_idx, occ_unmatched_dets_idx = self.assign(occluded_lost_tracks_reid_dist,
|
| 152 |
+
thresh=match_conditions['reid_occluded_thr'])
|
| 153 |
+
|
| 154 |
+
matches_idx_2_1 = np.concatenate([vis_matched_idx, occ_matched_idx])
|
| 155 |
+
unmatched_tracks_idx_2_1 = np.concatenate([vis_unmatched_tracks_idx, occ_unmatched_tracks_idx])
|
| 156 |
+
unmatched_dets_idx_2_1 = np.concatenate([vis_unmatched_dets_idx, occ_unmatched_dets_idx])
|
| 157 |
+
|
| 158 |
+
if not used_occluded:
|
| 159 |
+
matches_idx_2_1, unmatched_tracks_idx_2_1, unmatched_dets_idx_2_1 = self.assign(lost_tracks_reid_dist,
|
| 160 |
+
thresh=match_conditions['reid_thr'])
|
| 161 |
+
if match_conditions['iou_thr'] >= 0:
|
| 162 |
+
matches_idx_2_2, unmatched_tracks_idx_2_2, unmatched_dets_idx_2_2 = self.assign(lost_tracks_iou_dist,
|
| 163 |
+
thresh=match_conditions['iou_thr'])
|
| 164 |
+
# take the intersection of matches_idx_2_1 and matches_idx_2_2
|
| 165 |
+
matches_idx_2_1 = matches_idx_2_1.tolist()
|
| 166 |
+
matches_idx_2_2 = matches_idx_2_2.tolist()
|
| 167 |
+
|
| 168 |
+
# merge the two result
|
| 169 |
+
merged_matches_idx = list()
|
| 170 |
+
for _track in matches_idx_2_1:
|
| 171 |
+
if _track in matches_idx_2_2:
|
| 172 |
+
merged_matches_idx.append(_track)
|
| 173 |
+
else:
|
| 174 |
+
merged_matches_idx = matches_idx_2_1.tolist()
|
| 175 |
+
|
| 176 |
+
# convert local merge idx to global ones
|
| 177 |
+
global_matches_idx = []
|
| 178 |
+
for idx, match in enumerate(merged_matches_idx):
|
| 179 |
+
global_matches_idx.append([unmatched_tracks_idx[match[0]], unmatched_dets_idx[match[1]]])
|
| 180 |
+
if len(global_matches_idx):
|
| 181 |
+
global_matches_idx = np.array(global_matches_idx)
|
| 182 |
+
else:
|
| 183 |
+
global_matches_idx = np.empty((0, 2), dtype=np.int64)
|
| 184 |
+
|
| 185 |
+
# convert to input (global) indices
|
| 186 |
+
if len(merged_matches_idx):
|
| 187 |
+
merged_matches_idx = np.array(merged_matches_idx)
|
| 188 |
+
else:
|
| 189 |
+
merged_matches_idx = np.empty((0, 2), dtype=np.int64)
|
| 190 |
+
|
| 191 |
+
global_unmatched_tracks_idx = []
|
| 192 |
+
global_unmatched_dets_idx = []
|
| 193 |
+
|
| 194 |
+
# update unmatch track
|
| 195 |
+
for idx, _track_idx in enumerate(unmatched_tracks_idx):
|
| 196 |
+
if idx in merged_matches_idx[:, 0]:
|
| 197 |
+
continue
|
| 198 |
+
else:
|
| 199 |
+
global_unmatched_tracks_idx.append(_track_idx)
|
| 200 |
+
|
| 201 |
+
# update unmatch det
|
| 202 |
+
for idx, _det_idx in enumerate(unmatched_dets_idx):
|
| 203 |
+
if idx in merged_matches_idx[:, 1]:
|
| 204 |
+
continue
|
| 205 |
+
else:
|
| 206 |
+
global_unmatched_dets_idx.append(_det_idx)
|
| 207 |
+
|
| 208 |
+
global_unmatched_tracks_idx = np.array(global_unmatched_tracks_idx)
|
| 209 |
+
global_unmatched_dets_idx = np.array(global_unmatched_dets_idx)
|
| 210 |
+
return global_matches_idx, global_unmatched_tracks_idx, global_unmatched_dets_idx
|
| 211 |
+
|
| 212 |
+
def matching_dists(self, tracks: List[Tracklet],
|
| 213 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 214 |
+
""" Compute the distance between tracklets and detections"""
|
| 215 |
+
return self.dist(tracks, dets)
|
| 216 |
+
|
| 217 |
+
def matching_scores(self, tracks: List[Tracklet],
|
| 218 |
+
dets: List[Tracklet]) -> np.ndarray:
|
| 219 |
+
""" Compute the matching scores between tracklets and detections"""
|
| 220 |
+
return self.dist.matching_scores(tracks, dets)
|
| 221 |
+
|
| 222 |
+
def assign(self, distances: np.ndarray, thresh: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 223 |
+
return linear_assignment(distances, thresh=thresh)
|
| 224 |
+
|
models/models/trackers/reid_parallel_tracker/matchers/single_stage_matcher.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import List, Tuple, Dict
|
| 3 |
+
from ..core.matching import linear_assignment
|
| 4 |
+
from ..core.tracklet import Tracklet
|
| 5 |
+
from .distances import DistCosine
|
| 6 |
+
|
| 7 |
+
class SingleStageMatcher():
|
| 8 |
+
def __init__(self,
|
| 9 |
+
dist_high_cfg: Dict,
|
| 10 |
+
dist_low_cfg: Dict,
|
| 11 |
+
match_thr: float):
|
| 12 |
+
""" Perform matching with high score detection boxes and low score detection boxes in a single step
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
- dist_high_cfg (Dict): distance fucntion to compute the matching score between tracklets and high score detection boxes
|
| 16 |
+
- dist_low_cfg (Dict): distance function to compute the matching score between tracklets and low score detection boxes. This function should be stricter than dist_high_cfg as low_detection is less reliable than high_detection.
|
| 17 |
+
- match_thr (float): matching distance threshold. Lower value means stricter matching.
|
| 18 |
+
"""
|
| 19 |
+
self.dist_high = DistCosine(**dist_high_cfg)
|
| 20 |
+
self.dist_low = DistCosine(**dist_low_cfg)
|
| 21 |
+
self.match_thr = match_thr
|
| 22 |
+
|
| 23 |
+
def __call__(self,
|
| 24 |
+
tracks: List[Tracklet],
|
| 25 |
+
dets_high: List[Tracklet],
|
| 26 |
+
dets_low: List[Tracklet]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 27 |
+
""" Associate with tracklets with detection boxes"""
|
| 28 |
+
dist_high = self.dist_high(tracks, dets_high)
|
| 29 |
+
dist_low = self.dist_low(tracks, dets_low)
|
| 30 |
+
if np.prod(dist_high.shape) >0 and np.prod(dist_low.shape) >0:
|
| 31 |
+
beta = dist_high.max()/dist_low.max()
|
| 32 |
+
dists = np.concatenate([dist_high,beta*dist_low],axis=1)
|
| 33 |
+
else:
|
| 34 |
+
if np.prod(dist_high.shape) >0:
|
| 35 |
+
dists = dist_high
|
| 36 |
+
else:
|
| 37 |
+
dists = dist_low
|
| 38 |
+
matches_idx, unmatched_tracks_idx, unmatched_dets_idx = linear_assignment(dists, thresh=self.match_thr)
|
| 39 |
+
return matches_idx, unmatched_tracks_idx, unmatched_dets_idx
|
models/models/trackers/reid_parallel_tracker/parallel_tracker.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from typing import Tuple, Dict,List
|
| 3 |
+
import numpy as np
|
| 4 |
+
from .core.tracklet import TrackState
|
| 5 |
+
from mmcv.ops import bbox_overlaps
|
| 6 |
+
import torch
|
| 7 |
+
from .three_stage_tracker import ThreeStageTracker
|
| 8 |
+
from .matchers.distances import DistCosine
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ParallelTracker(ThreeStageTracker):
|
| 12 |
+
def __init__(self,
|
| 13 |
+
reid_filter_det=None,
|
| 14 |
+
*args, **kwargs):
|
| 15 |
+
super().__init__(*args, **kwargs)
|
| 16 |
+
# remove duplicated boxes
|
| 17 |
+
self.reid_filter_det = reid_filter_det
|
| 18 |
+
if reid_filter_det is not None:
|
| 19 |
+
self.reid_filter_det_dist = DistCosine(**reid_filter_det['dist_cfg'])
|
| 20 |
+
else:
|
| 21 |
+
self.reid_filter_det_dist = None
|
| 22 |
+
self.history_changes = []
|
| 23 |
+
|
| 24 |
+
def merge_active_lost_tracks(self,
|
| 25 |
+
dets_high:np.array,
|
| 26 |
+
active_tracks:List,
|
| 27 |
+
lost_tracks:List,
|
| 28 |
+
matched_active_track_indices:np.array,
|
| 29 |
+
matched_lost_track_indices:np.array):
|
| 30 |
+
|
| 31 |
+
"""
|
| 32 |
+
The merge_active_lost_tracks merges active and lost tracks based on matching detection results, updating their associated
|
| 33 |
+
indices. The merging is guided by certain criteria, such as distance comparisons between tracks and
|
| 34 |
+
detections, and whether the tracks share active frames.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
|
| 38 |
+
dets_high (array): An array representing high-confidence detections.
|
| 39 |
+
active_tracks (list): A list of active track objects.
|
| 40 |
+
lost_tracks (list): A list of lost track objects.
|
| 41 |
+
matched_active_track_indices (array): An array of matched indices between active tracks and detections.
|
| 42 |
+
matched_lost_track_indices (array): An array of matched indices between lost tracks and detections.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
|
| 46 |
+
A dictionary containing various arrays representing different aspects of the merging process:
|
| 47 |
+
unmerged_active_track_indices: Array of unmatched active track indices that are not merged with lost tracks and cannot match with any detection.
|
| 48 |
+
unmerged_lost_track_indices: Array of unmatched lost track indices that are not merged with active tracks and cannot match with any detection.
|
| 49 |
+
matched_pairs_active: Array of matched indices between active tracks and detections after the merging process.
|
| 50 |
+
matched_pairs_lost: Array of matched indices between lost tracks and detections after the merging process.
|
| 51 |
+
active_tracks_match_lost_tracks_detection: Array of active track indices that were matched to the same detection as lost tracks.
|
| 52 |
+
merged_lost_indices: Array of lost track indices that were merged with active tracks.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
unmerged_active_track_indices = []
|
| 56 |
+
unmerged_lost_track_indices = []
|
| 57 |
+
matched_pairs_active = [] # Match_pairs 1
|
| 58 |
+
matched_pairs_lost = [] # Match_pairs 1
|
| 59 |
+
active_tracks_match_lost_tracks_detection = []
|
| 60 |
+
merged_lost_indices = []
|
| 61 |
+
|
| 62 |
+
# Active/lost tracks match to different detections --> match_pairs_1
|
| 63 |
+
matched_active_det_indices = matched_active_track_indices[:, 1].tolist(
|
| 64 |
+
)
|
| 65 |
+
matched_lost_det_indices = matched_lost_track_indices[:, 1].tolist()
|
| 66 |
+
|
| 67 |
+
for idx, det_idx in enumerate(matched_active_det_indices):
|
| 68 |
+
if not det_idx in matched_lost_det_indices:
|
| 69 |
+
matched_pairs_active.append(matched_active_track_indices[idx])
|
| 70 |
+
|
| 71 |
+
for idx, det_idx in enumerate(matched_lost_det_indices):
|
| 72 |
+
if not det_idx in matched_active_det_indices:
|
| 73 |
+
matched_pairs_lost.append(matched_lost_track_indices[idx])
|
| 74 |
+
|
| 75 |
+
# detection --matches--> row-th of active track in matched_active_track_indices[:, 0]
|
| 76 |
+
det_active_rowmatrix_dict = dict()
|
| 77 |
+
for idx, (_active_trackidx, _detidx) in enumerate(matched_active_track_indices):
|
| 78 |
+
det_active_rowmatrix_dict[_detidx] = idx
|
| 79 |
+
|
| 80 |
+
# detection --matches--> row-th of lost track in matched_lost_track_indices[:, 0]
|
| 81 |
+
det_lost_rowmatrix_dict = dict()
|
| 82 |
+
for idx, (_lost_trackidx, _detidx) in enumerate(matched_lost_track_indices):
|
| 83 |
+
det_lost_rowmatrix_dict[_detidx] = idx
|
| 84 |
+
|
| 85 |
+
# calculate distance between active_tracks and dets_high
|
| 86 |
+
active_dists = self.matcher_active.matching_dists(active_tracks,
|
| 87 |
+
dets_high)
|
| 88 |
+
# calculate distance between lost_tracks and dets_high
|
| 89 |
+
lost_dists = self.matcher_lost.matching_dists(lost_tracks,
|
| 90 |
+
dets_high)
|
| 91 |
+
|
| 92 |
+
# -------------------------- Main Loop ----------------------------------
|
| 93 |
+
for detidx, row_lost_idx in det_lost_rowmatrix_dict.items():
|
| 94 |
+
is_matched_to_the_same_detection = detidx in det_active_rowmatrix_dict
|
| 95 |
+
if is_matched_to_the_same_detection:
|
| 96 |
+
row_active_idx = det_active_rowmatrix_dict[detidx]
|
| 97 |
+
# one detection --> 2 tracks
|
| 98 |
+
active_track_idx = matched_active_track_indices[row_active_idx][0]
|
| 99 |
+
lost_track_idx = matched_lost_track_indices[row_lost_idx][0]
|
| 100 |
+
|
| 101 |
+
# Both active in the past at the same time?
|
| 102 |
+
both_active_in_the_past_at_the_same_time = active_tracks[active_track_idx].common_active_frames(
|
| 103 |
+
lost_tracks[lost_track_idx])
|
| 104 |
+
|
| 105 |
+
if both_active_in_the_past_at_the_same_time:
|
| 106 |
+
active_dist = active_dists[active_track_idx, detidx]
|
| 107 |
+
lost_dist = lost_dists[lost_track_idx, detidx]
|
| 108 |
+
# active_track-->det has smaller distance than lost_track-->det
|
| 109 |
+
is_active_track_give_smaller_dist = active_dist <= lost_dist
|
| 110 |
+
if is_active_track_give_smaller_dist:
|
| 111 |
+
# assign detection to active track
|
| 112 |
+
matched_pairs_active.append(
|
| 113 |
+
matched_active_track_indices[row_active_idx])
|
| 114 |
+
unmerged_lost_track_indices.append(
|
| 115 |
+
lost_track_idx)
|
| 116 |
+
else:
|
| 117 |
+
# assign detection to lost track
|
| 118 |
+
matched_pairs_lost.append(
|
| 119 |
+
matched_lost_track_indices[row_lost_idx])
|
| 120 |
+
unmerged_active_track_indices.append(
|
| 121 |
+
active_track_idx)
|
| 122 |
+
else:
|
| 123 |
+
# ------------------- Merge trajectories --------------------------------------
|
| 124 |
+
# the active track will be removed, match this detection for lost tracklet
|
| 125 |
+
active_tracks_match_lost_tracks_detection.append(
|
| 126 |
+
active_track_idx)
|
| 127 |
+
matched_pairs_lost.append(
|
| 128 |
+
matched_lost_track_indices[row_lost_idx])
|
| 129 |
+
merged_lost_indices.append(lost_track_idx)
|
| 130 |
+
# update active frames for lost track
|
| 131 |
+
lost_tracks[lost_track_idx].update_active_frames(
|
| 132 |
+
active_tracks[active_track_idx].active_frames)
|
| 133 |
+
|
| 134 |
+
# convert to numpy array
|
| 135 |
+
unmerged_active_track_indices = np.array(
|
| 136 |
+
unmerged_active_track_indices)
|
| 137 |
+
unmerged_lost_track_indices = np.array(
|
| 138 |
+
unmerged_lost_track_indices)
|
| 139 |
+
matched_pairs_active = np.array(matched_pairs_active).reshape(-1, 2)
|
| 140 |
+
matched_pairs_lost = np.array(matched_pairs_lost).reshape(-1, 2)
|
| 141 |
+
active_tracks_match_lost_tracks_detection = np.array(
|
| 142 |
+
active_tracks_match_lost_tracks_detection)
|
| 143 |
+
merged_lost_indices = np.array(merged_lost_indices)
|
| 144 |
+
|
| 145 |
+
# return values
|
| 146 |
+
return dict(
|
| 147 |
+
unmerged_active_track_indices=unmerged_active_track_indices,
|
| 148 |
+
unmerged_lost_track_indices=unmerged_lost_track_indices,
|
| 149 |
+
matched_pairs_active=matched_pairs_active, # Match_pairs 1
|
| 150 |
+
matched_pairs_lost=matched_pairs_lost, # Match_pairs 1
|
| 151 |
+
active_tracks_match_lost_tracks_detection=active_tracks_match_lost_tracks_detection,
|
| 152 |
+
merged_lost_indices=merged_lost_indices,
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
def update(self,
|
| 156 |
+
det_result: Dict,
|
| 157 |
+
Hmat: np.array = None,
|
| 158 |
+
meta_data: Dict = None) -> Tuple[Dict, Dict]:
|
| 159 |
+
"""
|
| 160 |
+
The update function is the main method for performing tracking. It takes a detection result, a homography transformation matrix (optional),
|
| 161 |
+
and additional meta-data (optional) as input and returns the active tracks and lost tracks in the current frame after updating
|
| 162 |
+
their states and associations with new detections.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
|
| 166 |
+
det_result (dict): A dictionary representing the detection result from a detector.
|
| 167 |
+
Hmat (np.array, optional): A NumPy array representing the homography transformation matrix.
|
| 168 |
+
meta_data (dict, optional): Additional meta-data that may be used during the tracking process.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
|
| 172 |
+
active_tracks (dict): A dictionary representing the tracked active tracks in the current frame.
|
| 173 |
+
lost_tracks (dict): A dictionary representing the lost tracks in the current frame.
|
| 174 |
+
modifications (dict): A dictionary containing modifications made during the tracking process.
|
| 175 |
+
"""
|
| 176 |
+
self.frame_id += 1
|
| 177 |
+
|
| 178 |
+
# Step 1: Split the detections into high score/lower score group
|
| 179 |
+
det_result = self.preprocess_det_result(det_result)
|
| 180 |
+
|
| 181 |
+
# remove duplicated detection by ReID
|
| 182 |
+
if self.reid_filter_det_dist is not None:
|
| 183 |
+
det_result = self.remove_det_by_reid(det_result)
|
| 184 |
+
|
| 185 |
+
dets_high, dets_low = self.split_detections_by_scores(det_result)
|
| 186 |
+
|
| 187 |
+
# Step 2: Split the tracks into active_tracks, lost_tracks, and unconfirmed (just initialize)
|
| 188 |
+
active_tracks, lost_tracks, unconfirmed = self.split_tracks_by_activation()
|
| 189 |
+
|
| 190 |
+
# - predict the current location with KF, and compensate for Camera Motion
|
| 191 |
+
self.predict_with_gmc(active_tracks, lost_tracks, unconfirmed, Hmat)
|
| 192 |
+
|
| 193 |
+
# Step 3: Matching Stage 1.1 - Association with high score detection boxes and active tracks
|
| 194 |
+
matched_active_track_indices, unmatched_active_track_indices, unmatched_active_det_indices \
|
| 195 |
+
= self.matcher_active(active_tracks, dets_high)
|
| 196 |
+
|
| 197 |
+
# Step 4: Matching Stage 1.2 - Association with high score detection boxes and lost tracks
|
| 198 |
+
matched_lost_track_indices, unmatch_lost_track_indices, unmatch_lost_det_indices = self.matcher_lost(
|
| 199 |
+
lost_tracks, dets_high)
|
| 200 |
+
|
| 201 |
+
# Step 5. assing the Lost status to the unmatch_tracks
|
| 202 |
+
lost_stracks = [active_tracks[it]
|
| 203 |
+
for it in unmatched_active_track_indices if active_tracks[it].state != TrackState.Lost]
|
| 204 |
+
for track in lost_stracks:
|
| 205 |
+
track.mark_lost()
|
| 206 |
+
|
| 207 |
+
# Step 6. Merge active tracks and lost tracks
|
| 208 |
+
merged_results = self.merge_active_lost_tracks(dets_high,
|
| 209 |
+
active_tracks,
|
| 210 |
+
lost_tracks,
|
| 211 |
+
matched_active_track_indices,
|
| 212 |
+
matched_lost_track_indices)
|
| 213 |
+
|
| 214 |
+
unmerged_active_track_indices = merged_results[
|
| 215 |
+
'unmerged_active_track_indices']
|
| 216 |
+
unmerged_lost_track_indices = merged_results['unmerged_lost_track_indices']
|
| 217 |
+
match_pairs_1_1 = merged_results['matched_pairs_active']
|
| 218 |
+
match_pairs_1_2 = merged_results['matched_pairs_lost']
|
| 219 |
+
active_tracks_match_lost_tracks_detection = merged_results[
|
| 220 |
+
'active_tracks_match_lost_tracks_detection']
|
| 221 |
+
merged_lost_indices = merged_results['merged_lost_indices']
|
| 222 |
+
|
| 223 |
+
# Step 7. Assign the Lost status to the Merged unmatched active tracks
|
| 224 |
+
lost_stracks_1 = [active_tracks[it]
|
| 225 |
+
for it in unmerged_active_track_indices if active_tracks[it].state != TrackState.Lost]
|
| 226 |
+
for track in lost_stracks_1:
|
| 227 |
+
track.mark_lost()
|
| 228 |
+
lost_stracks.extend(lost_stracks_1)
|
| 229 |
+
|
| 230 |
+
# Step 8. update Match_pairs 1
|
| 231 |
+
activated_stracks, refind_stracks = self.update_matched_tracks(
|
| 232 |
+
match_pairs_1_1, active_tracks, dets_high)
|
| 233 |
+
|
| 234 |
+
activated_stracks_1, refind_stracks_1 = self.update_matched_tracks(
|
| 235 |
+
match_pairs_1_2, lost_tracks, dets_high)
|
| 236 |
+
|
| 237 |
+
activated_stracks.extend(activated_stracks_1)
|
| 238 |
+
refind_stracks.extend(refind_stracks_1)
|
| 239 |
+
|
| 240 |
+
# Step 9. Remove active tracks that are merged into lost tracks
|
| 241 |
+
merged_active_tracks = [active_tracks[idx]
|
| 242 |
+
for idx in active_tracks_match_lost_tracks_detection]
|
| 243 |
+
for track in merged_active_tracks:
|
| 244 |
+
track.mark_removed()
|
| 245 |
+
|
| 246 |
+
# for post processing: later step will know how to change switch ID back to the lost one
|
| 247 |
+
need_replaced_track_info = [
|
| 248 |
+
_m_track.history_info for _m_track in merged_active_tracks]
|
| 249 |
+
new_track_info = [
|
| 250 |
+
lost_tracks[_idx].history_info for _idx in merged_lost_indices]
|
| 251 |
+
|
| 252 |
+
# Step 10. Take Unmatched dets in both list
|
| 253 |
+
merge_unmatched_det_indices = list(set(unmatched_active_det_indices.tolist()).intersection(
|
| 254 |
+
set(unmatch_lost_det_indices.tolist())))
|
| 255 |
+
|
| 256 |
+
# remain unmatch detection
|
| 257 |
+
unmatched_dets_2 = [
|
| 258 |
+
dets_high[_idx] for _idx in merge_unmatched_det_indices]
|
| 259 |
+
|
| 260 |
+
# Step 11: Matching Stage 2 - Association between new detections and unconfirmed tracks (tracks just initialized in the previous frame)
|
| 261 |
+
matched_pairs_2, unmatch_track_indices_2, unmatched_high_det_indices_2 = self.matcher_unconfirmed(
|
| 262 |
+
unconfirmed, unmatched_dets_2)
|
| 263 |
+
|
| 264 |
+
for itracked, idet in matched_pairs_2:
|
| 265 |
+
# Update matches_unconfirmed into active_tracks
|
| 266 |
+
unconfirmed[itracked].update(unmatched_dets_2[idet], self.frame_id)
|
| 267 |
+
activated_stracks.append(unconfirmed[itracked])
|
| 268 |
+
|
| 269 |
+
# Step 12. remove unconfirmed tracks that do not match any detections
|
| 270 |
+
removed_stracks = [unconfirmed[it] for it in unmatch_track_indices_2]
|
| 271 |
+
for track in removed_stracks:
|
| 272 |
+
track.mark_removed()
|
| 273 |
+
|
| 274 |
+
all_remove_tracks = removed_stracks + merged_active_tracks
|
| 275 |
+
# Step 13. init new stracks if they are high score and not too small boxes as tentative tracks (unconfirmed)
|
| 276 |
+
new_tracks = self.init_new_tracks(
|
| 277 |
+
unmatched_dets_2, unmatched_high_det_indices_2)
|
| 278 |
+
|
| 279 |
+
# new_tracks that have very high score and not occluded with current tracks will be directly activated
|
| 280 |
+
self.activate_new_tracks(
|
| 281 |
+
new_tracks, activated_stracks + refind_stracks)
|
| 282 |
+
|
| 283 |
+
# Step 14: remove lost tracks if they are already lost for a certain frames
|
| 284 |
+
self.lost_stracks.extend(lost_stracks)
|
| 285 |
+
removed_lost_stracks = self.remove_lost_tracks()
|
| 286 |
+
all_remove_tracks += removed_lost_stracks
|
| 287 |
+
|
| 288 |
+
# Step 15: Final result merging
|
| 289 |
+
active_tracks, lost_tracks = self.merge_results(
|
| 290 |
+
activated_stracks, refind_stracks, new_tracks, all_remove_tracks)
|
| 291 |
+
|
| 292 |
+
# update lost frame num
|
| 293 |
+
self.update_lost_frame()
|
| 294 |
+
|
| 295 |
+
modifications = dict(
|
| 296 |
+
need_replaced_track_info=need_replaced_track_info,
|
| 297 |
+
new_track_info=new_track_info
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
return active_tracks, lost_tracks, modifications
|
| 301 |
+
|
| 302 |
+
def remove_det_by_reid(self, det_result:Dict):
|
| 303 |
+
"""
|
| 304 |
+
The remove_det_by_reid function is a method of a class that filters out detection results based on the
|
| 305 |
+
ReID (Person Re-identification) distances between detected objects. It removes similar detections, keeping
|
| 306 |
+
only those that are dissimilar according to the ReID distance threshold and certain object characteristics
|
| 307 |
+
like bounding box overlap and detection scores.
|
| 308 |
+
|
| 309 |
+
Args:
|
| 310 |
+
|
| 311 |
+
det_result (dictionary): A dictionary containing the detection results.
|
| 312 |
+
Returns:
|
| 313 |
+
|
| 314 |
+
filter_det_result (dictionary): A dictionary containing the filtered detection results after applying the ReID-based filtering.
|
| 315 |
+
"""
|
| 316 |
+
all_embeddings = det_result['embeddings']
|
| 317 |
+
reid_dists = self.reid_filter_det_dist(all_embeddings, all_embeddings)
|
| 318 |
+
np.fill_diagonal(reid_dists, 1e4)
|
| 319 |
+
|
| 320 |
+
# det boxes and score
|
| 321 |
+
boxes = det_result['boxes']
|
| 322 |
+
boxes = boxes.reshape(-1, 5)
|
| 323 |
+
boxes_tensor = torch.from_numpy(boxes)[:, :-1]
|
| 324 |
+
|
| 325 |
+
# score matrix
|
| 326 |
+
det_scores = boxes[:, -1]
|
| 327 |
+
det_scores_matrix = det_scores.reshape(-1,
|
| 328 |
+
1) > det_scores.reshape(1, -1)
|
| 329 |
+
|
| 330 |
+
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
|
| 331 |
+
width = x2 - x1
|
| 332 |
+
height = y2 - y1
|
| 333 |
+
areas = np.abs(width * height)
|
| 334 |
+
det_area_matrix = areas.reshape(-1, 1) > areas.reshape(1, -1)
|
| 335 |
+
|
| 336 |
+
box_iof = bbox_overlaps(boxes_tensor, boxes_tensor, mode='iof').numpy()
|
| 337 |
+
np.fill_diagonal(box_iof, 0.0)
|
| 338 |
+
iof_non_overllaped = box_iof < self.reid_filter_det['iof_thr']
|
| 339 |
+
|
| 340 |
+
reid_dists[iof_non_overllaped] = 1e4
|
| 341 |
+
# reid_dists[det_scores_matrix] = 1e4
|
| 342 |
+
reid_dists[det_area_matrix] = 1e4
|
| 343 |
+
|
| 344 |
+
remove_det_matrix = reid_dists < self.reid_filter_det['reid_dist_thr']
|
| 345 |
+
remove_det_matrix = remove_det_matrix.sum(1) > 0
|
| 346 |
+
remove_det_idx = np.nonzero(remove_det_matrix)[0]
|
| 347 |
+
# draw_queries_galleries(det_result['obj_imgs'], det_result['obj_imgs'], reid_dists, det_result['frame_id'])
|
| 348 |
+
|
| 349 |
+
if len(remove_det_idx):
|
| 350 |
+
filter_det_result = dict()
|
| 351 |
+
filter_det_result['frame_id'] = det_result['frame_id']
|
| 352 |
+
for key, val in det_result.items():
|
| 353 |
+
filter_val = []
|
| 354 |
+
if key in ['frame_id']:
|
| 355 |
+
continue
|
| 356 |
+
for idx in range(len(val)):
|
| 357 |
+
if not idx in remove_det_idx:
|
| 358 |
+
filter_val.append(val[idx])
|
| 359 |
+
filter_det_result[key] = np.array(filter_val)
|
| 360 |
+
else:
|
| 361 |
+
filter_det_result = det_result
|
| 362 |
+
|
| 363 |
+
return filter_det_result
|
| 364 |
+
|
| 365 |
+
def remove_rows(self, matrix:np.array, row_indices:List, C=2):
|
| 366 |
+
"""
|
| 367 |
+
The remove_rows function takes a 2D matrix as input and removes specific rows from
|
| 368 |
+
the matrix based on the provided row indices. The resulting matrix is then returned with a reshaped number of columns.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
|
| 372 |
+
matrix (numpy array): A 2D numpy array representing the input matrix.
|
| 373 |
+
row_indices (list): A list of integers representing the row indices to be removed from the matrix.
|
| 374 |
+
C (int, optional): An integer representing the number of columns in the output matrix.
|
| 375 |
+
Returns:
|
| 376 |
+
|
| 377 |
+
result_matrix (numpy array): The resulting 2D numpy array after removing the specified rows and reshaping with the
|
| 378 |
+
specified number of columns (C).
|
| 379 |
+
"""
|
| 380 |
+
row_indices = sorted(row_indices, reverse=True)
|
| 381 |
+
matrix = matrix.tolist()
|
| 382 |
+
for row_idx in row_indices:
|
| 383 |
+
matrix.pop(row_idx)
|
| 384 |
+
return np.array(matrix).reshape(-1, C)
|
| 385 |
+
|
| 386 |
+
def update_unmatch_track_det(self, tracklets:List, dets:List, matches_indices:np.array, remove_indices:List=[]):
|
| 387 |
+
"""
|
| 388 |
+
The update_unmatch_track_det updates and retrieves the indices of unmatched tracklets
|
| 389 |
+
and detections based on the provided matched indices and any specified indices for removal.
|
| 390 |
+
|
| 391 |
+
Args:
|
| 392 |
+
|
| 393 |
+
tracklets (list): A list of tracklet objects representing existing tracks.
|
| 394 |
+
dets (list): A list of detection objects representing detected items.
|
| 395 |
+
matches_indices (numpy array): A 2D numpy array containing matched indices between tracklets and detections.
|
| 396 |
+
remove_indices (list, optional): A list of integers representing indices of tracklets to be removed.
|
| 397 |
+
|
| 398 |
+
Returns:
|
| 399 |
+
|
| 400 |
+
unmatched_track_indices (numpy array): A 1D numpy array containing the indices of unmatched tracklets (tracks that do not
|
| 401 |
+
have any matches with detections).
|
| 402 |
+
unmatched_det_indices (numpy array): A 1D numpy array containing the indices of unmatched detections (detections that do
|
| 403 |
+
not have any matches with tracklets).
|
| 404 |
+
"""
|
| 405 |
+
# update unmatch track, det
|
| 406 |
+
unmatched_track_indices, unmatched_det_indices = [], []
|
| 407 |
+
for _track_idx in range(len(tracklets)):
|
| 408 |
+
if _track_idx in remove_indices:
|
| 409 |
+
continue
|
| 410 |
+
if not _track_idx in matches_indices[:, 0]:
|
| 411 |
+
unmatched_track_indices.append(_track_idx)
|
| 412 |
+
for _det_idx in range(len(dets)):
|
| 413 |
+
if not _det_idx in matches_indices[:, 1]:
|
| 414 |
+
unmatched_det_indices.append(_det_idx)
|
| 415 |
+
unmatched_track_indices = np.array(unmatched_track_indices)
|
| 416 |
+
unmatched_det_indices = np.array(unmatched_det_indices)
|
| 417 |
+
|
| 418 |
+
return unmatched_track_indices, unmatched_det_indices
|
| 419 |
+
|
| 420 |
+
def det_local_aware_spliter(self, dets:List, mar_top:float=0.2, mar_left:float=0.25,
|
| 421 |
+
mar_right:float=0.25, h:int=1876, w:int=2896):
|
| 422 |
+
"""
|
| 423 |
+
The det_local_aware_spliter function is a method of a class. It splits a list of detections into two separate lists based on their positions within a specified region of interest in an image.
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
|
| 427 |
+
dets (list): A list of detection objects representing detected items.
|
| 428 |
+
mar_top (float, optional): A floating-point value representing the margin ratio from the top of the image.
|
| 429 |
+
mar_left (float, optional): A floating-point value representing the margin ratio from the left side of the image.
|
| 430 |
+
mar_right (float, optional): A floating-point value representing the margin ratio from the right side of the image.
|
| 431 |
+
h (int, optional): An integer representing the height of the image.
|
| 432 |
+
w (int, optional): An integer representing the width of the image.
|
| 433 |
+
Returns:
|
| 434 |
+
|
| 435 |
+
det_in (list): A list of detection objects that lie inside the specified region of interest.
|
| 436 |
+
det_out (list): A list of detection objects that lie outside the specified region of interest.
|
| 437 |
+
"""
|
| 438 |
+
det_in, det_out = [], []
|
| 439 |
+
for det in dets:
|
| 440 |
+
t, l, b, r = det.tlbr
|
| 441 |
+
x, y = l+(r-l)/2, t+(b-t)/2
|
| 442 |
+
if (x < w*(1-mar_right)) and (x > w*mar_left) and (y > h*mar_top):
|
| 443 |
+
det_in.append(det)
|
| 444 |
+
else:
|
| 445 |
+
det_out.append(det)
|
| 446 |
+
return det_in, det_out
|
| 447 |
+
|
| 448 |
+
def split_size(self, dets:list, thr:float=0.1, h:int=1876, w:int=2896):
|
| 449 |
+
"""
|
| 450 |
+
The split_size function is a method of a class. It separates a list of detection objects into two separate lists based on their height relative to a specified threshold, as a proportion of the image height.
|
| 451 |
+
|
| 452 |
+
Args:
|
| 453 |
+
|
| 454 |
+
dets (list): A list of detection objects representing detected items.
|
| 455 |
+
thr (float, optional): A floating-point value representing the threshold for splitting detections based on height.
|
| 456 |
+
h (int, optional): An integer representing the height of the image.
|
| 457 |
+
w (int, optional): An integer representing the width of the image.
|
| 458 |
+
Returns:
|
| 459 |
+
|
| 460 |
+
det_small (list): A list of detection objects whose height is smaller than the specified threshold.
|
| 461 |
+
det_large (list): A list of detection objects whose height is larger than or equal to the specified threshold.
|
| 462 |
+
"""
|
| 463 |
+
det_small, det_large = [], []
|
| 464 |
+
for det in dets:
|
| 465 |
+
t, _, b, _ = det.tlbr
|
| 466 |
+
if b-t < thr*h:
|
| 467 |
+
det_small.append(det)
|
| 468 |
+
else:
|
| 469 |
+
det_large.append(det)
|
| 470 |
+
return det_small, det_large
|
models/models/trackers/reid_parallel_tracker/three_stage_tracker.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from typing import Tuple, Dict
|
| 3 |
+
import numpy as np
|
| 4 |
+
from .core.tracklet import TrackState
|
| 5 |
+
from .core.tracklet import (Tracklet, TrackState)
|
| 6 |
+
from .base_tracker import BaseTracker
|
| 7 |
+
from .matchers.base_matchers import SimMatcher
|
| 8 |
+
|
| 9 |
+
class ThreeStageTracker(BaseTracker):
|
| 10 |
+
def __init__(self,
|
| 11 |
+
matcher_active_cfg = dict(match_thr=0.5),
|
| 12 |
+
matcher_det_low_cfg = dict(match_thr=0.5),
|
| 13 |
+
matcher_lost_cfg = dict(match_thr=0.5),
|
| 14 |
+
matcher_unconfirmed_cfg = dict(match_thr=0.5),
|
| 15 |
+
enable_reid_buffer = False,
|
| 16 |
+
*args,**kwargs):
|
| 17 |
+
super().__init__(*args,**kwargs)
|
| 18 |
+
self.matcher_active = SimMatcher(**matcher_active_cfg)
|
| 19 |
+
self.matcher_det_low = SimMatcher(**matcher_det_low_cfg)
|
| 20 |
+
self.matcher_lost = SimMatcher(**matcher_lost_cfg)
|
| 21 |
+
self.matcher_unconfirmed = SimMatcher(**matcher_unconfirmed_cfg)
|
| 22 |
+
self.enable_reid_buffer = enable_reid_buffer
|
| 23 |
+
|
| 24 |
+
def split_tracks_by_activation(self):
|
| 25 |
+
""" Split the tracks into active_tracks, lost_tracks, and unconfirmed (just initialize)
|
| 26 |
+
Returns:
|
| 27 |
+
strack_pool: List[Tracklet]
|
| 28 |
+
unconfirmed: List[Tracklet]
|
| 29 |
+
"""
|
| 30 |
+
unconfirmed = []
|
| 31 |
+
tracked_stracks = [] # type: list[Tracklet]
|
| 32 |
+
for track in self.tracked_stracks:
|
| 33 |
+
if not track.is_activated:
|
| 34 |
+
unconfirmed.append(track)
|
| 35 |
+
else:
|
| 36 |
+
tracked_stracks.append(track)
|
| 37 |
+
return tracked_stracks, self.lost_stracks, unconfirmed
|
| 38 |
+
|
| 39 |
+
def predict_with_gmc(self, active_tracks, lost_tracks, unconfirmed, Hmat):
|
| 40 |
+
Tracklet.multi_predict(active_tracks)
|
| 41 |
+
Tracklet.multi_predict(lost_tracks)
|
| 42 |
+
if Hmat is not None:
|
| 43 |
+
Tracklet.multi_gmc(active_tracks, Hmat)
|
| 44 |
+
Tracklet.multi_gmc(lost_tracks, Hmat)
|
| 45 |
+
Tracklet.multi_gmc(unconfirmed, Hmat)
|
| 46 |
+
|
| 47 |
+
def update(self,
|
| 48 |
+
det_result: Dict,
|
| 49 |
+
Hmat: np.array=None,
|
| 50 |
+
meta_data: Dict=None) -> Tuple[Dict, Dict]:
|
| 51 |
+
""" The main function to perform tracking. The pipeline is similar to ByteTrack/BoTSort,
|
| 52 |
+
which first associate with high score detection boxes, and then associate with low score detection boxes.
|
| 53 |
+
Args:
|
| 54 |
+
det_result (dict): detection result from detector
|
| 55 |
+
Hmat (np.array, optional): Homography transformation matrix. Defaults to None.
|
| 56 |
+
Returns:
|
| 57 |
+
active_tracks (dict): tracked tracks in the current frame. See format_track_results for the format
|
| 58 |
+
lost_tracks (dict): lost tracks in the current frame. See format_track_results for the format
|
| 59 |
+
"""
|
| 60 |
+
self.frame_id += 1
|
| 61 |
+
# Step 1: Split the detections into high score/lower score group
|
| 62 |
+
det_result = self.preprocess_det_result(det_result)
|
| 63 |
+
dets_high, dets_low = self.split_detections_by_scores(det_result)
|
| 64 |
+
|
| 65 |
+
# Step 2: Split the tracks into trackpool=(active_tracks + lost_tracks) and unconfirmed (just initialize)
|
| 66 |
+
active_tracks, lost_tracks, unconfirmed = self.split_tracks_by_activation()
|
| 67 |
+
# - predict the current location with KF, and compensate for Camera Motion
|
| 68 |
+
self.predict_with_gmc(active_tracks, lost_tracks, unconfirmed, Hmat)
|
| 69 |
+
|
| 70 |
+
# Step 3: First association with high score detection boxes and active track
|
| 71 |
+
match_idxes, unmatch_track_idxes, unmatch_det_idxes= self.matcher_active(active_tracks, dets_high)
|
| 72 |
+
activated_stracks, refind_stracks = self.update_matched_tracks(match_idxes, active_tracks, dets_high) # refind_track=[]
|
| 73 |
+
|
| 74 |
+
# Step 4: Second association with low score detection boxes"""
|
| 75 |
+
unmatch_tracks = [active_tracks[idx] for idx in unmatch_track_idxes if active_tracks[idx].state == TrackState.Tracked]
|
| 76 |
+
|
| 77 |
+
match_idxes_2, unmatch_track_idxes_2, _ = self.matcher_det_low(unmatch_tracks, dets_low)
|
| 78 |
+
activated_stracks_2, refind_stracks_2 = self.update_matched_tracks(match_idxes_2, unmatch_tracks, dets_low)
|
| 79 |
+
activated_stracks.extend(activated_stracks_2 )
|
| 80 |
+
refind_stracks.extend(refind_stracks_2)
|
| 81 |
+
|
| 82 |
+
# - assing the Lost status to the unmatch_tracks that are not matched with low score detection boxes
|
| 83 |
+
lost_stracks = [unmatch_tracks[it] for it in unmatch_track_idxes_2 if unmatch_tracks[it].state != TrackState.Lost]
|
| 84 |
+
for track in lost_stracks: track.mark_lost()
|
| 85 |
+
|
| 86 |
+
# Step 5: Second association, between remain high detections and lost tracks
|
| 87 |
+
remain_dets = [dets_high[i] for i in unmatch_det_idxes]
|
| 88 |
+
match_idxes_3, unmatch_track_idxes_3, unmatch_det_idxes_3 = self.matcher_lost(lost_tracks, remain_dets)
|
| 89 |
+
activated_stracks_3, refind_stracks_3 = self.update_matched_tracks(match_idxes_3, lost_tracks, remain_dets)
|
| 90 |
+
activated_stracks.extend(activated_stracks_3,)
|
| 91 |
+
refind_stracks.extend(refind_stracks_3)
|
| 92 |
+
unmatch_lost_tracks = [lost_tracks[idx] for idx in unmatch_track_idxes_3]
|
| 93 |
+
lost_stracks.extend(unmatch_lost_tracks)
|
| 94 |
+
|
| 95 |
+
# Step 6: Second association, between new detections and lost tracks (tracks just initialized in the previous frame)
|
| 96 |
+
remain_dets_3 = [remain_dets[i] for i in unmatch_det_idxes_3]
|
| 97 |
+
match_idxes_4, unmatch_track_idxes_4, unmatch_det_idxes_4 = self.matcher_unconfirmed(unconfirmed, remain_dets_3)
|
| 98 |
+
|
| 99 |
+
for itracked, idet in match_idxes_4:
|
| 100 |
+
# Update matches_unconfirmed into active_tracks
|
| 101 |
+
unconfirmed[itracked].update(remain_dets_3[idet], self.frame_id)
|
| 102 |
+
activated_stracks.append(unconfirmed[itracked])
|
| 103 |
+
|
| 104 |
+
# - remove unconfirmed tracks that do not match any detections
|
| 105 |
+
removed_stracks = [unconfirmed[it] for it in unmatch_track_idxes_4]
|
| 106 |
+
for track in removed_stracks: track.mark_removed()
|
| 107 |
+
|
| 108 |
+
# - init new stracks if they are high score and not too small boxes as tentative tracks (unconfirmed)
|
| 109 |
+
new_tracks = self.init_new_tracks(remain_dets_3, unmatch_det_idxes_4)
|
| 110 |
+
# - new_tracks that have very high score and not occluded with current tracks will be directly activated
|
| 111 |
+
self.activate_new_tracks(new_tracks, activated_stracks + refind_stracks)
|
| 112 |
+
|
| 113 |
+
# Step 6: remove lost tracks if they are already lost for a certain frames
|
| 114 |
+
self.lost_stracks.extend(lost_stracks)
|
| 115 |
+
removed_lost_stracks = self.remove_lost_tracks()
|
| 116 |
+
removed_stracks += removed_lost_stracks
|
| 117 |
+
|
| 118 |
+
# Step 7: Final result merging
|
| 119 |
+
active_tracks, lost_tracks = self.merge_results(activated_stracks, refind_stracks, new_tracks, removed_stracks)
|
| 120 |
+
|
| 121 |
+
# update lost frame num
|
| 122 |
+
self.update_lost_frame()
|
| 123 |
+
|
| 124 |
+
return active_tracks,lost_tracks
|
| 125 |
+
|
| 126 |
+
def update_lost_frame(self):
|
| 127 |
+
|
| 128 |
+
for track in self.tracked_stracks:
|
| 129 |
+
track.reset_lost_frame_num()
|
| 130 |
+
|
| 131 |
+
for track in self.lost_stracks:
|
| 132 |
+
track.set_lost_frame_num()
|
models/reids/__init__.py
ADDED
|
File without changes
|
models/reids/solider.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Tuple, List
|
| 2 |
+
from models.base.trt_base import TRT_Base
|
| 3 |
+
import torch
|
| 4 |
+
import cv2
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
class SOLIDERBase():
|
| 8 |
+
def __init__(self,
|
| 9 |
+
preprocess_cfg: Dict=dict(
|
| 10 |
+
mean=[0.5, 0.5, 0.5],
|
| 11 |
+
std=[0.5, 0.5, 0.5]
|
| 12 |
+
),
|
| 13 |
+
use_torch: bool=False):
|
| 14 |
+
""" SOLIDERBase class for inference.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
preprocess_cfg (Dict):
|
| 18 |
+
- mean (List[float, float, float]): mean offset values for preprocessing.
|
| 19 |
+
- std (List[float, float, float]): standard deviation offset values for preprocessing.
|
| 20 |
+
use_torch (bool): use torch tensor or numpy array in preprocess and postprocess function.
|
| 21 |
+
"""
|
| 22 |
+
self.preprocess_cfg = preprocess_cfg
|
| 23 |
+
self.use_torch = use_torch
|
| 24 |
+
|
| 25 |
+
def preprocess(self, input_data: np.ndarray):
|
| 26 |
+
""" Preprocess function for input data.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
input_data (np.ndarray): batch input image.
|
| 30 |
+
"""
|
| 31 |
+
tensor_data = []
|
| 32 |
+
if ((isinstance(input_data, np.ndarray)) and (len(input_data.shape) == 3)):
|
| 33 |
+
input_data = [input_data]
|
| 34 |
+
for i in range(len(input_data)):
|
| 35 |
+
img = input_data[i]
|
| 36 |
+
img = cv2.resize(img, self.input_shape[2:][::-1], interpolation=cv2.INTER_LINEAR)
|
| 37 |
+
height, width = img.shape[0], img.shape[1]
|
| 38 |
+
if self.use_torch:
|
| 39 |
+
tensor_data.append(torch.from_numpy(img).to(self.device))
|
| 40 |
+
else:
|
| 41 |
+
tensor_data.append(img)
|
| 42 |
+
if self.use_torch:
|
| 43 |
+
mean = torch.tensor(mean).to(self.device)
|
| 44 |
+
std = torch.tensor(std).to(self.device)
|
| 45 |
+
tensor_data = (torch.stack(tensor_data, dim=0)[:, :, :, [2, 1, 0]]/255.0 - mean)/std
|
| 46 |
+
tensor_data = tensor_data.permute(0, 3, 1, 2).float().contiguous()/255.0
|
| 47 |
+
else:
|
| 48 |
+
mean = np.array(mean)
|
| 49 |
+
std = np.array(std)
|
| 50 |
+
tensor_data = (np.stack(tensor_data, axis=0)[:, :, :, [2, 1, 0]]/255.0 - mean)/std
|
| 51 |
+
return tensor_data, height, width
|
| 52 |
+
|
| 53 |
+
class SOLIDERTRT(TRT_Base, SOLIDERBase):
|
| 54 |
+
def __init__(self,
|
| 55 |
+
preprocess_cfg: Dict,
|
| 56 |
+
img_shape: Tuple[int, int]=(384, 128),
|
| 57 |
+
batch_size: int=32,
|
| 58 |
+
model_path: str="",
|
| 59 |
+
device: str='0',):
|
| 60 |
+
""" SOLIDER TRT class for inference, which is based on TRT_Base and SOLIDERBase.
|
| 61 |
+
"""
|
| 62 |
+
self.img_shape = img_shape
|
| 63 |
+
self.batch_size = batch_size
|
| 64 |
+
input_shape = (self.batch_size, *self.img_shape)
|
| 65 |
+
super().__init__(input_shape, model_path, device)
|
| 66 |
+
SOLIDERBase.__init__(self, preprocess_cfg=preprocess_cfg, use_torch=True)
|
| 67 |
+
|
| 68 |
+
def infer_batch(self, image_batch: np.ndarray) -> List[np.ndarray]:
|
| 69 |
+
""" Batch inference function for batch input image.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
image_batch (np.ndarray): batch of input image.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
tensor_data, height, width = self.preprocess(image_batch)
|
| 76 |
+
self.change_runtime_dimension(input_shape=(len(tensor_data), 3, height, width))
|
| 77 |
+
self.model['binding_addrs']['images'] = int(tensor_data.data_ptr())
|
| 78 |
+
self.model['context'].execute_v2(list(self.model['binding_addrs'].values()))
|
| 79 |
+
feats = self.model['bindings']['feats'].data.cpu()
|
| 80 |
+
|
| 81 |
+
reid_outputs = []
|
| 82 |
+
for idx in range(len(feats)):
|
| 83 |
+
feat = feats[idx]
|
| 84 |
+
reid_outputs.append({"feat": feat.float().numpy()})
|
| 85 |
+
return reid_outputs
|
| 86 |
+
|
| 87 |
+
|
models/trackers/__init__.py
ADDED
|
File without changes
|
models/trackers/byte_track.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mmtrack.models.trackers.byte_tracker import ByteTracker as MMByteTracker
|
| 2 |
+
from mmtrack.models.builder import build_motion
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
import numpy as np
|
| 5 |
+
import mmcv
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class BYTETracker(MMByteTracker):
|
| 9 |
+
def __init__(self, *args, **kwargs):
|
| 10 |
+
""" ByteTracker class for tracking.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
obj_score_thrs (Dict):
|
| 14 |
+
- high (float): if detection box > high -> high_score_detections for first association.
|
| 15 |
+
- low (float): if low < detection box < high -> low_score_detections for second association.
|
| 16 |
+
init_track_thr (float): Detection score threshold for initializing a new tracklet.
|
| 17 |
+
weight_iou_with_det_scores (bool): Whether using detection scores to weight IOU which is used for matching.
|
| 18 |
+
match_iou_thrs (Dict): IOU distance threshold for matching between two frames.
|
| 19 |
+
- high (float): Threshold of the first matching.
|
| 20 |
+
- low (float): Threshold of the second matching.
|
| 21 |
+
- tentative (float): Threshold of the matching for tentative tracklets.
|
| 22 |
+
num_frames_retain (int): If a track is disappeared more than num_frames_retain frames, it will be deleted in the memo.
|
| 23 |
+
motion (Dict): Config for motion.
|
| 24 |
+
- type (str): Motion type (KalmanFilter, LinearFilter).
|
| 25 |
+
"""
|
| 26 |
+
self.motion = build_motion(kwargs.pop("motion"))
|
| 27 |
+
super().__init__(*args, **kwargs)
|
| 28 |
+
|
| 29 |
+
def track_batch(self, start_frame_idx: int, det_results: List[Dict], conf_thres: float=0.0):
|
| 30 |
+
""" Batch inference function for batch det results.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
start_frame_idx (int): start_frame_idx of this batch.
|
| 34 |
+
det_results (List[Dict]): detection results of this batch.
|
| 35 |
+
conf_thres (float): If a tracklet's confidence < confidence threshold, it will be removed.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
track_outputs = []
|
| 39 |
+
for frame_id, frame_det_outputs in enumerate(det_results):
|
| 40 |
+
boxes, labels = frame_det_outputs.pop("boxes"), frame_det_outputs.pop("labels")
|
| 41 |
+
|
| 42 |
+
boxes, labels, track_ids = self.track(None, None, self,
|
| 43 |
+
bboxes=boxes, labels=labels,
|
| 44 |
+
frame_id=frame_id+start_frame_idx,
|
| 45 |
+
rescale=False)
|
| 46 |
+
boxes = np.around(boxes.numpy(),decimals=3)
|
| 47 |
+
labels = labels.numpy().astype(np.uint)
|
| 48 |
+
track_ids = track_ids.numpy().astype(np.uint)
|
| 49 |
+
idxs = np.where(boxes[:, 4] > conf_thres)[0]
|
| 50 |
+
track_outputs.append({
|
| 51 |
+
"boxes": boxes[idxs],
|
| 52 |
+
"labels": labels[idxs],
|
| 53 |
+
"ids": track_ids[idxs]
|
| 54 |
+
})
|
| 55 |
+
return track_outputs
|
| 56 |
+
|
projects/human_detection/ReadMe.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HUMAN DETECTION DEMO
|
| 2 |
+
|
| 3 |
+
### 1. Prepare data and weights:
|
| 4 |
+
The weights and sample test video files can be downloaded from:
|
| 5 |
+
+ NAS at `/5_project_internal/19_Demo_App/human_detection`.
|
| 6 |
+
+ On HPC2,`/data/cc-demo/human_detection/weights/`
|
| 7 |
+
|
| 8 |
+
If you run the demo on other machine, please download the files and put it the same path as HPC2.
|
| 9 |
+
From now on, we will assume that you have data and weights in the following structure:
|
| 10 |
+
```
|
| 11 |
+
/data/cc-demo/human_detection/
|
| 12 |
+
├── sample_inputs
|
| 13 |
+
│ ├── 1.mp4
|
| 14 |
+
│ ├── 2.mp4
|
| 15 |
+
│ └── 3.mp4
|
| 16 |
+
└── weights
|
| 17 |
+
└── mmyolov8_s_human_dynamic_shape.onnx
|
| 18 |
+
mmyolov8_s_human_DBsx640x800.trt
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
### 2. Compile ONNX & TRT model
|
| 23 |
+
#### 2.1. Start docker container
|
| 24 |
+
For Developer that want to add more functions to the codebase, and use Docker as Developing environment, use:
|
| 25 |
+
```
|
| 26 |
+
bash docker_run.sh
|
| 27 |
+
docker attach <docker_container_name>
|
| 28 |
+
```
|
| 29 |
+
After attaching into docker, do the following step to export onnx and trt model
|
| 30 |
+
```
|
| 31 |
+
bash projects/human_detection/export_onnx_trt/export_trt_mmyolov8.sh
|
| 32 |
+
```
|
| 33 |
+
It will take about 20minutes to comple ONNX & TRT model, and the file will be stored at folder `/data/human_detection\deploy`
|
| 34 |
+
|
| 35 |
+
### 3. Start Gradio
|
| 36 |
+
a. From inside Docker
|
| 37 |
+
```
|
| 38 |
+
bash projects/human_detection/demo_app.sh
|
| 39 |
+
```
|
| 40 |
+
b. From Host machine
|
| 41 |
+
```
|
| 42 |
+
bash projects/human_detection/docker_run.sh
|
| 43 |
+
```
|
projects/human_detection/demo_app.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from models.detectors.yolov7 import YOLOv7ONNX,YOLOv7TRT
|
| 4 |
+
from models.detectors.mmyolov8 import MMYOLOv8ONNX,MMYOLOv8TRT
|
| 5 |
+
from projects.human_detection.engine.pipeline import run_e2e_pipeline
|
| 6 |
+
from mmcv import VideoReader
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
title = "Human Detection - CYBERCORE AI DEMO"
|
| 10 |
+
description = "Human Monitoring Demo: It will run detection, tracking on input video and show output video.\nYou may click on of the examples or upload your own image."
|
| 11 |
+
|
| 12 |
+
root_folder = "/data/human_detection/"
|
| 13 |
+
example_video_paths = [
|
| 14 |
+
os.path.join(root_folder,"sample_inputs/1.mp4"),
|
| 15 |
+
os.path.join(root_folder,"sample_inputs/2.mp4"),
|
| 16 |
+
os.path.join(root_folder,"sample_inputs/3.mp4"),
|
| 17 |
+
]
|
| 18 |
+
output_dir = os.path.join(root_folder,"outputs")
|
| 19 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 20 |
+
assert any([os.path.exists(example_video_path) for example_video_path in example_video_paths]), f"Example video does not exist. Please download example videos from NAS:[https://gofile.me/6ZWyr/q0saLr8hb] and put them in the following path 'human_detection/inputs'"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
#---------------------Model Path----------------------------------
|
| 24 |
+
# model_path_yolov7_onnx = os.path.join(root_folder, "weights/yolov7_pedestrian_480x640.onnx") # model_path yolov7
|
| 25 |
+
# model_path_yolov7_trt = os.path.join(root_folder, "weights/yolov7_pedestrian_480x640.trt") # model_path yolov7
|
| 26 |
+
# assert os.path.exists(model_path_yolov7_onnx) or os.path.exists(model_path_yolov7_trt), f"Model does not exist. Please download model from NAS:[path], compile TRT, and put it in the following path 'project_folder'/weights/yolov7-honda-pedestrian_deploy_v2_0.1_768x1280.onnx"
|
| 27 |
+
model_path_yolov8_onnx = os.path.join(root_folder, "weights/mmyolov8_s_human_dynamic_shape.onnx") # model_path yolov8
|
| 28 |
+
model_path_yolov8_trt = os.path.join(root_folder, "weights/mmyolov8_s_human_DBsx640x800.trt") # model_path yolov8
|
| 29 |
+
assert os.path.exists(model_path_yolov8_onnx) or os.path.exists(model_path_yolov8_trt), f"Model does not exist. Please download model from NAS:[path], compile TRT, and put it in the following path 'project_folder'/weights/yolov8-human.onnx"
|
| 30 |
+
|
| 31 |
+
# ---------------------Configs----------------------------------
|
| 32 |
+
use_trt_yolov8 = os.path.exists(model_path_yolov8_trt)
|
| 33 |
+
yolov8_cfg = dict(
|
| 34 |
+
img_shape=(3, 640, 800),
|
| 35 |
+
batch_size=32,
|
| 36 |
+
preprocess_cfg=dict(
|
| 37 |
+
border_color=(114, 114, 114),
|
| 38 |
+
auto=False,
|
| 39 |
+
scaleFill=False,
|
| 40 |
+
scaleup=False,
|
| 41 |
+
stride=32),
|
| 42 |
+
nms_agnostic_cfg=dict(
|
| 43 |
+
type='nms',
|
| 44 |
+
iou_threshold=0.6,
|
| 45 |
+
class_agnostic=True),
|
| 46 |
+
score_thr=0.1,
|
| 47 |
+
model_path = model_path_yolov8_trt if use_trt_yolov8 else model_path_yolov8_onnx,
|
| 48 |
+
device='0'
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# use_trt_yolov7 = os.path.exists(model_path_yolov7_trt)
|
| 52 |
+
# yolov7_cfg = dict(
|
| 53 |
+
# img_shape=(3, 480, 640),
|
| 54 |
+
# batch_size=32,
|
| 55 |
+
# preprocess_cfg=dict(
|
| 56 |
+
# border_color=(114, 114, 114),
|
| 57 |
+
# auto=False,
|
| 58 |
+
# scaleFill=False,
|
| 59 |
+
# scaleup=True,
|
| 60 |
+
# stride=32),
|
| 61 |
+
# nms_agnostic_cfg=dict(
|
| 62 |
+
# type='nms',
|
| 63 |
+
# iou_threshold=0.99,
|
| 64 |
+
# class_agnostic=True),
|
| 65 |
+
# model_path = model_path_yolov7_trt if use_trt_yolov7 else model_path_yolov7_onnx,
|
| 66 |
+
# device='0'
|
| 67 |
+
# )
|
| 68 |
+
|
| 69 |
+
tracker_cfg = dict(
|
| 70 |
+
obj_score_thrs=dict(high=0.6, low=0.1),
|
| 71 |
+
init_track_thr=0.7,
|
| 72 |
+
weight_iou_with_det_scores=True,
|
| 73 |
+
match_iou_thrs=dict(high=0.1, low=0.5, tentative=0.3),
|
| 74 |
+
num_frames_retain=30,
|
| 75 |
+
motion=dict(type='KalmanFilter')
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
visualizer_cfg = dict(fps=-1, min_width=1280)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# yolov7_detector = YOLOv7TRT(**yolov7_cfg) if use_trt_yolov7 else YOLOv7ONNX(**yolov7_cfg)
|
| 82 |
+
yolov8_detector = MMYOLOv8TRT(**yolov8_cfg) if use_trt_yolov8 else MMYOLOv8ONNX(**yolov8_cfg)
|
| 83 |
+
|
| 84 |
+
def inference(video, model_name, conf_thres, show_conf, progress=gr.Progress()):
|
| 85 |
+
output_path = os.path.join(output_dir, os.path.basename(video))
|
| 86 |
+
if os.path.exists(output_path):
|
| 87 |
+
os.remove(output_path)
|
| 88 |
+
# detector = yolov7_detector if model_name == "pedestrian" else yolov8_detector
|
| 89 |
+
detector = yolov8_detector
|
| 90 |
+
run_e2e_pipeline(video, detector, tracker_cfg, visualizer_cfg, output_path, conf_thres, show_conf, progress)
|
| 91 |
+
return output_path
|
| 92 |
+
|
| 93 |
+
def clear(*all_components):
|
| 94 |
+
outputs = [None]*len(all_components)
|
| 95 |
+
return outputs
|
| 96 |
+
|
| 97 |
+
# ---------------------Gradio UI----------------------------------
|
| 98 |
+
with gr.Blocks(title=title) as demo:
|
| 99 |
+
gr.Markdown("<h1 style='text-align: center; margin-bottom: 1rem'>" + title + "</h1>")
|
| 100 |
+
gr.Markdown(description)
|
| 101 |
+
input_components = []
|
| 102 |
+
output_components = []
|
| 103 |
+
|
| 104 |
+
with gr.Row():
|
| 105 |
+
input_video = gr.Video(type="file", label="input_video")
|
| 106 |
+
output_video = gr.Video(label="output_video")
|
| 107 |
+
input_components.append(input_video)
|
| 108 |
+
output_components.append(output_video)
|
| 109 |
+
|
| 110 |
+
with gr.Row().style(equal_height=True, mobile_collapse=True):
|
| 111 |
+
with gr.Column(scale=2, variant="panel") as input_column:
|
| 112 |
+
model_dropdown = gr.Dropdown(label="Detector Model",
|
| 113 |
+
choices=["pedestrian", "general-human"],
|
| 114 |
+
default="general-human",
|
| 115 |
+
info="Choose the application model for Human detection.")
|
| 116 |
+
prob_threshold_slider = gr.components.Slider(minimum=0, maximum=1.0, step=0.01, value=0.3, label="Confidence Threshold")
|
| 117 |
+
show_confidence = gr.Checkbox(label="Show Confidence")
|
| 118 |
+
input_components.extend([model_dropdown, prob_threshold_slider, show_confidence])
|
| 119 |
+
|
| 120 |
+
with gr.Column(scale=2):
|
| 121 |
+
examples_handler = gr.Examples(
|
| 122 |
+
examples=[[item] for item in example_video_paths],
|
| 123 |
+
fn=inference,
|
| 124 |
+
inputs=input_components,
|
| 125 |
+
outputs=output_components,
|
| 126 |
+
examples_per_page=3
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
with gr.Row():
|
| 130 |
+
submit_btn = gr.Button("Submit", variant="primary")
|
| 131 |
+
clear_btn = gr.Button("Clear")
|
| 132 |
+
|
| 133 |
+
submit_btn.click(
|
| 134 |
+
inference,
|
| 135 |
+
input_components,
|
| 136 |
+
output_components,
|
| 137 |
+
api_name="predict",
|
| 138 |
+
scroll_to_output=True,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
clear_btn.click(
|
| 142 |
+
clear,
|
| 143 |
+
input_components + output_components,
|
| 144 |
+
input_components + output_components,
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
demo.queue(concurrency_count=3).launch(share=True, server_name='0.0.0.0', server_port=7860)
|
projects/human_detection/docker_run.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker run to start run demo app for traffic_monitoring demo
|
| 2 |
+
read -p "Please enter your container name, for example 'cc-demo': " name
|
| 3 |
+
read -p "Please enter your data directory path, for example /data/cc-demo/: " data
|
| 4 |
+
read -p "Please enter your public port, for example 8585: " pubport
|
| 5 |
+
|
| 6 |
+
docker run --name $name --shm-size=8g --gpus all --rm -it \
|
| 7 |
+
-p $pubport:7860 \
|
| 8 |
+
-v $data:/data \
|
| 9 |
+
-v $(pwd):/root/workspace/cc-demo \
|
| 10 |
+
-w /root/workspace/cc-demo \
|
| 11 |
+
cybercorecloud/cc-demo:v2.1 /bin/bash -c "cd projects && gradio human_detection/demo_app.py"
|
projects/human_detection/engine/pipeline.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from queue import Queue
|
| 2 |
+
from typing import List, Dict, Tuple
|
| 3 |
+
from threading import Thread, Event
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
import sys
|
| 7 |
+
from models.engine.threading_func import batch_extract_thread, detect_thread, bytetrack_thread, update_progress_thread
|
| 8 |
+
from .threading_func import visualize_thread
|
| 9 |
+
from models.detectors.yolov7 import YOLOv7TRT
|
| 10 |
+
import gradio as gr
|
| 11 |
+
|
| 12 |
+
def init_logger(task: str, logging_level=logging.INFO):
|
| 13 |
+
log_format = '[%(levelname)s][%(process)d] [%(threadName)s] [%(asctime)s] %(message)s'
|
| 14 |
+
|
| 15 |
+
# Console log handler
|
| 16 |
+
console_handler = logging.StreamHandler(stream=sys.stdout)
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging_level,
|
| 19 |
+
format=log_format,
|
| 20 |
+
datefmt='%d/%b/%Y %H:%M:%S',
|
| 21 |
+
handlers=[console_handler])
|
| 22 |
+
|
| 23 |
+
return logging.getLogger(task)
|
| 24 |
+
|
| 25 |
+
def run_e2e_pipeline(video_path: str,
|
| 26 |
+
detector: YOLOv7TRT,
|
| 27 |
+
tracker_cfg: Dict,
|
| 28 |
+
visualizer_cfg: Dict,
|
| 29 |
+
output_path: str,
|
| 30 |
+
conf_thres: float,
|
| 31 |
+
show_conf: bool=True,
|
| 32 |
+
progress: gr.Progress=None,
|
| 33 |
+
batch_size: int=32):
|
| 34 |
+
""" Function to run full pipelines (batch_extract, detection, tracking, counting, visualization, update progress). It will start threads and wait threads finish.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
video_path (str): input video path that needs to be processed.
|
| 38 |
+
detector (YOLOv7TRT): Detector to run detection on input video.
|
| 39 |
+
tracker_cfg (Dict): tracker's config.
|
| 40 |
+
visualizer_cfg (Dict): visualizer's config.
|
| 41 |
+
output_path (str): path of output video.
|
| 42 |
+
conf_thres (float): If a tracklet's confidence < confidence threshold, it will be removed.
|
| 43 |
+
show_conf (bool): Visualize confidence of track results or not.
|
| 44 |
+
progress (gr.Progress): Gradio's Progress that needs to be updated.
|
| 45 |
+
batch_size (int): Size of a batch input images that needs to be processed.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
# init logger
|
| 49 |
+
init_logger("Infer")
|
| 50 |
+
video_name = os.path.basename(video_path)
|
| 51 |
+
|
| 52 |
+
img_batch_queue = Queue(maxsize=2)
|
| 53 |
+
vis_img_batch_queue = Queue(maxsize=2)
|
| 54 |
+
det_queue = Queue()
|
| 55 |
+
track_queue = Queue()
|
| 56 |
+
vis_queue = Queue()
|
| 57 |
+
eStop = Event()
|
| 58 |
+
|
| 59 |
+
pipeline = []
|
| 60 |
+
pipeline.append(Thread(target=batch_extract_thread,
|
| 61 |
+
name=f'DT Batch Thread-Video Name {video_name}',
|
| 62 |
+
args=(video_path, img_batch_queue, vis_img_batch_queue, eStop),
|
| 63 |
+
kwargs={"batch_size":batch_size}))
|
| 64 |
+
pipeline.append(Thread(target=detect_thread,
|
| 65 |
+
name=f'Detect Thread-Video Name {video_name}',
|
| 66 |
+
args=(detector, img_batch_queue, det_queue, eStop)))
|
| 67 |
+
pipeline.append(Thread(target=bytetrack_thread,
|
| 68 |
+
name=f'Track Thread-Video Name {video_name}',
|
| 69 |
+
args=(tracker_cfg, det_queue, track_queue, eStop, conf_thres)))
|
| 70 |
+
pipeline.append(Thread(target=visualize_thread,
|
| 71 |
+
name=f'Visualize Thread-Video Name {video_name}',
|
| 72 |
+
args=(visualizer_cfg, output_path, vis_img_batch_queue, track_queue, vis_queue, eStop, show_conf)))
|
| 73 |
+
if progress is not None:
|
| 74 |
+
pipeline.append(Thread(target=update_progress_thread,
|
| 75 |
+
name=f"Update Progress Thread-Video Name {video_name}",
|
| 76 |
+
args=(vis_queue, progress, eStop)))
|
| 77 |
+
|
| 78 |
+
for p in pipeline: p.start()
|
| 79 |
+
for p in pipeline: p.join()
|
| 80 |
+
del eStop
|
| 81 |
+
del pipeline
|
projects/human_detection/engine/threading_func.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from queue import Queue
|
| 2 |
+
from threading import Event
|
| 3 |
+
from models.engine.threading_func import queue_clear
|
| 4 |
+
|
| 5 |
+
from projects.human_detection.engine.visualizer import Visualizer
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def visualize_thread(visualizer_cfg, output_path, vis_img_batch_queue: Queue, track_queue: Queue, visualize_queue: Queue, eStop: Event, show_conf):
|
| 10 |
+
logging.info("Start Visualize Thread")
|
| 11 |
+
visualizer = Visualizer(**visualizer_cfg)
|
| 12 |
+
input_video_info = vis_img_batch_queue.get()
|
| 13 |
+
visualizer.init_writer(input_video_info, output_path)
|
| 14 |
+
visualize_queue.put(input_video_info)
|
| 15 |
+
track_item = track_queue.get()
|
| 16 |
+
start_frame_idx = -1
|
| 17 |
+
img_batch_item = vis_img_batch_queue.get()
|
| 18 |
+
while (img_batch_item is not None and track_item is not None):
|
| 19 |
+
if eStop.is_set(): break
|
| 20 |
+
start_frame_idx, img_batch = img_batch_item
|
| 21 |
+
track_start_frame_idx, track_result = track_item
|
| 22 |
+
if (start_frame_idx != track_start_frame_idx) or (len(img_batch) != len(track_result)):
|
| 23 |
+
error_msg=[501, f"Error when runing visualization at start_frame_idx {start_frame_idx}. "]
|
| 24 |
+
log_error_message = f"Error {error_msg[0]}: {error_msg[1]}"
|
| 25 |
+
logging.error(log_error_message)
|
| 26 |
+
eStop.set()
|
| 27 |
+
break
|
| 28 |
+
for idx, (frame, frame_track_result) in enumerate(zip(img_batch, track_result)):
|
| 29 |
+
|
| 30 |
+
visualizer.visualize(frame, frame_track_result, show_conf)
|
| 31 |
+
visualize_queue.put(start_frame_idx + idx)
|
| 32 |
+
img_batch_item = vis_img_batch_queue.get()
|
| 33 |
+
track_item = track_queue.get()
|
| 34 |
+
visualizer.close()
|
| 35 |
+
|
| 36 |
+
# Finish this thread
|
| 37 |
+
if eStop.is_set():
|
| 38 |
+
logging.warning(f"Early stop at start_frame_idx {start_frame_idx}")
|
| 39 |
+
queue_clear(visualize_queue)
|
| 40 |
+
else:
|
| 41 |
+
visualizer.convert()
|
| 42 |
+
logging.info(f"Finish visualize_thread.")
|
| 43 |
+
visualize_queue.put(None)
|
projects/human_detection/engine/visualizer.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from models.engine.visualizer import BaseVisualizer
|
| 5 |
+
|
| 6 |
+
class Visualizer(BaseVisualizer):
|
| 7 |
+
|
| 8 |
+
def __init__(self, fps: int=-1, min_width: int=-1):
|
| 9 |
+
""" Visualizer class for visualization (track_results + count_results).
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
class_map_ids (Dict): class mapping dictionary to map model's class to original class. Eg {0: 1, 1: 0, 2: 2, 3: 3} mean we swap class ID between 0 and 1.
|
| 13 |
+
fps (int): FPS for output video. If fps = -1, it will have same fps as input video.
|
| 14 |
+
min_width (int): minimum width for output video (height will be scaled to keep aspect ratio as input video). If min_width = -1, it will have same resolution as input video.
|
| 15 |
+
"""
|
| 16 |
+
class_names = ['pedestrian']
|
| 17 |
+
super().__init__(class_names, fps, min_width)
|
| 18 |
+
|
| 19 |
+
def visualize(self, img: np.ndarray, dettrack_at_frame_id: List[Dict]=None, show_conf: bool=True):
|
| 20 |
+
""" Function to visualize (track_results + count_results) a frame.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
img (np.ndarray): image need to be visualized.
|
| 24 |
+
dettrack_at_frame_id (List[Dict]): batch of track results which can be obtained from Tracker class.
|
| 25 |
+
show_conf (bool): Visualize confidence of track results or not.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
# Draw tracking
|
| 29 |
+
if (dettrack_at_frame_id):
|
| 30 |
+
boxes = dettrack_at_frame_id["boxes"]
|
| 31 |
+
# classes = dettrack_at_frame_id["labels"]
|
| 32 |
+
ids = dettrack_at_frame_id["ids"]
|
| 33 |
+
for bbox, id_ in zip(boxes, ids):
|
| 34 |
+
id_ = int(id_)
|
| 35 |
+
score = bbox[4]
|
| 36 |
+
color = self.get_color(id_)
|
| 37 |
+
label = f'{id_}' + (f' {score:.2f}' if (show_conf) else '')
|
| 38 |
+
tl, tf = 2, 1
|
| 39 |
+
c1, c2 = (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3]))
|
| 40 |
+
img = cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
| 41 |
+
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
| 42 |
+
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
| 43 |
+
img = cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)
|
| 44 |
+
img = cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
| 45 |
+
|
| 46 |
+
if (img.shape[0] != self.height or img.shape[1] != self.width):
|
| 47 |
+
img = cv2.resize(img, (self.width, self.height))
|
| 48 |
+
self.writer.write(img)
|
| 49 |
+
|
| 50 |
+
return img
|
| 51 |
+
|