HungKhoi commited on
Commit
a818fda
·
1 Parent(s): ab8806e

Init Upload

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +79 -0
  2. .gitignore +183 -0
  3. Dockerfile +131 -0
  4. models/__init__.py +0 -0
  5. models/base/__init__.py +2 -0
  6. models/base/onnx_base.py +58 -0
  7. models/base/trt_base.py +97 -0
  8. models/detectors/__init__.py +0 -0
  9. models/detectors/yolov7.py +228 -0
  10. models/models/__init__.py +0 -0
  11. models/models/base/__init__.py +0 -0
  12. models/models/base/onnx_base.py +68 -0
  13. models/models/base/trt_base.py +96 -0
  14. models/models/detectors/__init__.py +0 -0
  15. models/models/detectors/mmyolov8.py +86 -0
  16. models/models/detectors/yolov7.py +251 -0
  17. models/models/engine/__init__.py +0 -0
  18. models/models/engine/threading_func.py +221 -0
  19. models/models/engine/utils.py +248 -0
  20. models/models/engine/visualizer.py +143 -0
  21. models/models/pose/rtmpose.py +193 -0
  22. models/models/reids/__init__.py +0 -0
  23. models/models/reids/solider.py +165 -0
  24. models/models/trackers/__init__.py +2 -0
  25. models/models/trackers/byte_track.py +74 -0
  26. models/models/trackers/reid_parallel_tracker/__init__.py +1 -0
  27. models/models/trackers/reid_parallel_tracker/base_tracker.py +315 -0
  28. models/models/trackers/reid_parallel_tracker/core/__init__.py +0 -0
  29. models/models/trackers/reid_parallel_tracker/core/basetrack.py +165 -0
  30. models/models/trackers/reid_parallel_tracker/core/homography.py +89 -0
  31. models/models/trackers/reid_parallel_tracker/core/kalman_filter.py +275 -0
  32. models/models/trackers/reid_parallel_tracker/core/matching.py +405 -0
  33. models/models/trackers/reid_parallel_tracker/core/tracklet.py +427 -0
  34. models/models/trackers/reid_parallel_tracker/matchers/__init__.py +4 -0
  35. models/models/trackers/reid_parallel_tracker/matchers/base_matchers.py +39 -0
  36. models/models/trackers/reid_parallel_tracker/matchers/distances.py +132 -0
  37. models/models/trackers/reid_parallel_tracker/matchers/prioritize_reid_matcher.py +224 -0
  38. models/models/trackers/reid_parallel_tracker/matchers/single_stage_matcher.py +39 -0
  39. models/models/trackers/reid_parallel_tracker/parallel_tracker.py +470 -0
  40. models/models/trackers/reid_parallel_tracker/three_stage_tracker.py +132 -0
  41. models/reids/__init__.py +0 -0
  42. models/reids/solider.py +87 -0
  43. models/trackers/__init__.py +0 -0
  44. models/trackers/byte_track.py +56 -0
  45. projects/human_detection/ReadMe.md +43 -0
  46. projects/human_detection/demo_app.py +147 -0
  47. projects/human_detection/docker_run.sh +11 -0
  48. projects/human_detection/engine/pipeline.py +81 -0
  49. projects/human_detection/engine/threading_func.py +43 -0
  50. 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
+