jjyaoao commited on
Commit
3970d23
·
1 Parent(s): 2523e4a

Upload 62 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app.py +72 -0
  2. app.yml +11 -0
  3. pipeline/__init__.py +13 -0
  4. pipeline/cfg_utils.py +224 -0
  5. pipeline/config/infer_cfg_pphuman.yml +63 -0
  6. pipeline/config/tracker_config.yml +28 -0
  7. pipeline/datacollector.py +132 -0
  8. pipeline/download.py +340 -0
  9. pipeline/pipe_utils.py +263 -0
  10. pipeline/pipeline.py +898 -0
  11. pipeline/pphuman/action_infer.py +693 -0
  12. pipeline/pphuman/action_utils.py +114 -0
  13. pipeline/pphuman/attr_infer.py +348 -0
  14. pipeline/pphuman/mtmct.py +381 -0
  15. pipeline/pphuman/reid.py +204 -0
  16. pipeline/pphuman/video_action_infer.py +310 -0
  17. pipeline/pphuman/video_action_preprocess.py +545 -0
  18. pptracking/python/det_infer.py +594 -0
  19. pptracking/python/mot/__init__.py +25 -0
  20. pptracking/python/mot/matching/__init__.py +21 -0
  21. pptracking/python/mot/matching/deepsort_matching.py +382 -0
  22. pptracking/python/mot/matching/jde_matching.py +163 -0
  23. pptracking/python/mot/matching/ocsort_matching.py +129 -0
  24. pptracking/python/mot/motion/__init__.py +17 -0
  25. pptracking/python/mot/motion/kalman_filter.py +267 -0
  26. pptracking/python/mot/mtmct/__init__.py +24 -0
  27. pptracking/python/mot/mtmct/camera_utils.py +288 -0
  28. pptracking/python/mot/mtmct/postprocess.py +386 -0
  29. pptracking/python/mot/mtmct/utils.py +604 -0
  30. pptracking/python/mot/mtmct/zone.py +412 -0
  31. pptracking/python/mot/tracker/__init__.py +25 -0
  32. pptracking/python/mot/tracker/base_jde_tracker.py +286 -0
  33. pptracking/python/mot/tracker/base_sde_tracker.py +153 -0
  34. pptracking/python/mot/tracker/deepsort_tracker.py +185 -0
  35. pptracking/python/mot/tracker/jde_tracker.py +343 -0
  36. pptracking/python/mot/tracker/ocsort_tracker.py +366 -0
  37. pptracking/python/mot/utils.py +437 -0
  38. pptracking/python/mot/visualize.py +379 -0
  39. pptracking/python/mot_jde_infer.py +508 -0
  40. pptracking/python/mot_sde_infer.py +882 -0
  41. pptracking/python/mot_utils.py +349 -0
  42. pptracking/python/mtmct_cfg.yml +17 -0
  43. pptracking/python/picodet_postprocess.py +227 -0
  44. pptracking/python/preprocess.py +286 -0
  45. pptracking/python/tracker_config.yml +43 -0
  46. python/README.md +104 -0
  47. python/benchmark_utils.py +291 -0
  48. python/det_keypoint_unite_infer.py +377 -0
  49. python/det_keypoint_unite_utils.py +141 -0
  50. python/infer.py +1035 -0
app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import base64
3
+ from io import BytesIO
4
+ from PIL import Image
5
+ import numpy as np
6
+ import os
7
+ from pipeline.pipeline import pp_humanv2
8
+
9
+
10
+ # UGC: Define the inference fn() for your models
11
+ def model_inference(input_date, avtivity_list):
12
+
13
+ if isinstance(input_date, str):
14
+ if os.path.splitext(input_date)[-1] not in ['.avi','.mp4']:
15
+ return None
16
+
17
+ if 'do_entrance_counting'in avtivity_list or 'draw_center_traj' in avtivity_list:
18
+ if 'MOT' not in avtivity_list:
19
+ avtivity_list.append('MOT')
20
+
21
+ result = pp_humanv2(input_date, avtivity_list)
22
+
23
+ return result
24
+
25
+
26
+ def clear_all():
27
+ return None, None, None
28
+
29
+
30
+ with gr.Blocks() as demo:
31
+ gr.Markdown("PP-Human Pipeline")
32
+
33
+ with gr.Tabs():
34
+
35
+ with gr.TabItem("image"):
36
+
37
+ img_in = gr.Image(value="https://paddledet.bj.bcebos.com/modelcenter/images/PP-Human/human_attr.jpg",label="Input")
38
+ img_out = gr.Image(label="Output")
39
+
40
+ img_avtivity_list = gr.CheckboxGroup(["ATTR"])
41
+ img_button1 = gr.Button("Submit")
42
+ img_button2 = gr.Button("Clear")
43
+
44
+ with gr.TabItem("video"):
45
+
46
+ video_in = gr.Video(value="https://paddledet.bj.bcebos.com/modelcenter/images/PP-Human/human_attr.mp4",label="Input only support .mp4 or .avi")
47
+ video_out = gr.Video(label="Output")
48
+
49
+ video_avtivity_list = gr.CheckboxGroup(["MOT","ATTR","VIDEO_ACTION","SKELETON_ACTION","ID_BASED_DETACTION","ID_BASED_CLSACTION","REID",\
50
+ "do_entrance_counting","draw_center_traj"],label="Task Choice (note: only one task should be checked)")
51
+ video_button1 = gr.Button("Submit")
52
+ video_button2 = gr.Button("Clear")
53
+
54
+ img_button1.click(
55
+ fn=model_inference,
56
+ inputs=[img_in, img_avtivity_list],
57
+ outputs=img_out)
58
+ img_button2.click(
59
+ fn=clear_all,
60
+ inputs=None,
61
+ outputs=[img_in, img_out, img_avtivity_list])
62
+
63
+ video_button1.click(
64
+ fn=model_inference,
65
+ inputs=[video_in, video_avtivity_list],
66
+ outputs=video_out)
67
+ video_button2.click(
68
+ fn=clear_all,
69
+ inputs=None,
70
+ outputs=[video_in, video_out, video_avtivity_list])
71
+
72
+ demo.launch()
app.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 【PP-HumanV2-App-YAML】
2
+
3
+ APP_Info:
4
+ title: PP-HumanV2-App
5
+ colorFrom: blue
6
+ colorTo: yellow
7
+ sdk: gradio
8
+ sdk_version: 3.9
9
+ app_file: app.py
10
+ license: apache-2.0
11
+ device: cpu
pipeline/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
pipeline/cfg_utils.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import yaml
3
+ import copy
4
+ import argparse
5
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
6
+
7
+
8
+ class ArgsParser(ArgumentParser):
9
+ def __init__(self):
10
+ super(ArgsParser, self).__init__(
11
+ formatter_class=RawDescriptionHelpFormatter)
12
+ self.add_argument(
13
+ "-o", "--opt", nargs='*', help="set configuration options")
14
+
15
+ def parse_args(self, argv=None):
16
+ args = super(ArgsParser, self).parse_args(argv)
17
+ assert args.config is not None, \
18
+ "Please specify --config=configure_file_path."
19
+ args.opt = self._parse_opt(args.opt)
20
+ return args
21
+
22
+ def _parse_opt(self, opts):
23
+ config = {}
24
+ if not opts:
25
+ return config
26
+ for s in opts:
27
+ s = s.strip()
28
+ k, v = s.split('=', 1)
29
+ if '.' not in k:
30
+ config[k] = yaml.load(v, Loader=yaml.Loader)
31
+ else:
32
+ keys = k.split('.')
33
+ if keys[0] not in config:
34
+ config[keys[0]] = {}
35
+ cur = config[keys[0]]
36
+ for idx, key in enumerate(keys[1:]):
37
+ if idx == len(keys) - 2:
38
+ cur[key] = yaml.load(v, Loader=yaml.Loader)
39
+ else:
40
+ cur[key] = {}
41
+ cur = cur[key]
42
+ return config
43
+
44
+
45
+ def argsparser():
46
+ parser = ArgsParser()
47
+
48
+ parser.add_argument(
49
+ "--config",
50
+ type=str,
51
+ default='pipeline/config/infer_cfg_pphuman.yml',
52
+ help=("Path of configure"))
53
+ parser.add_argument(
54
+ "--image_file", type=str, default=None, help="Path of image file.")
55
+ parser.add_argument(
56
+ "--image_dir",
57
+ type=str,
58
+ default=None,
59
+ help="Dir of image file, `image_file` has a higher priority.")
60
+ parser.add_argument(
61
+ "--video_file",
62
+ type=str,
63
+ default=None,
64
+ help="Path of video file, `video_file` or `camera_id` has a highest priority."
65
+ )
66
+ parser.add_argument(
67
+ "--video_dir",
68
+ type=str,
69
+ default=None,
70
+ help="Dir of video file, `video_file` has a higher priority.")
71
+ parser.add_argument(
72
+ "--rtsp",
73
+ type=str,
74
+ nargs='+',
75
+ default=None,
76
+ help="list of rtsp inputs, for one or multiple rtsp input.")
77
+ parser.add_argument(
78
+ "--camera_id",
79
+ type=int,
80
+ default=-1,
81
+ help="device id of camera to predict.")
82
+ parser.add_argument(
83
+ "--output_dir",
84
+ type=str,
85
+ default="output",
86
+ help="Directory of output visualization files.")
87
+ parser.add_argument(
88
+ "--pushurl",
89
+ type=str,
90
+ default="",
91
+ help="url of output visualization stream.")
92
+ parser.add_argument(
93
+ "--run_mode",
94
+ type=str,
95
+ default='paddle',
96
+ help="mode of running(paddle/trt_fp32/trt_fp16/trt_int8)")
97
+ parser.add_argument(
98
+ "--device",
99
+ type=str,
100
+ default='cpu',
101
+ help="Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU."
102
+ )
103
+ parser.add_argument(
104
+ "--enable_mkldnn",
105
+ type=ast.literal_eval,
106
+ default=False,
107
+ help="Whether use mkldnn with CPU.")
108
+ parser.add_argument(
109
+ "--cpu_threads", type=int, default=1, help="Num of threads with CPU.")
110
+ parser.add_argument(
111
+ "--trt_min_shape", type=int, default=1, help="min_shape for TensorRT.")
112
+ parser.add_argument(
113
+ "--trt_max_shape",
114
+ type=int,
115
+ default=1280,
116
+ help="max_shape for TensorRT.")
117
+ parser.add_argument(
118
+ "--trt_opt_shape",
119
+ type=int,
120
+ default=640,
121
+ help="opt_shape for TensorRT.")
122
+ parser.add_argument(
123
+ "--trt_calib_mode",
124
+ type=bool,
125
+ default=False,
126
+ help="If the model is produced by TRT offline quantitative "
127
+ "calibration, trt_calib_mode need to set True.")
128
+ parser.add_argument(
129
+ "--do_entrance_counting",
130
+ type=bool,
131
+ default=False,
132
+ help="Whether counting the numbers of identifiers entering "
133
+ "or getting out from the entrance. Note that only support single-class MOT."
134
+ )
135
+ parser.add_argument(
136
+ "--do_break_in_counting",
137
+ type=bool,
138
+ default=False,
139
+ help="Whether counting the numbers of identifiers break in "
140
+ "the area. Note that only support single-class MOT and "
141
+ "the video should be taken by a static camera.")
142
+ parser.add_argument(
143
+ "--illegal_parking_time",
144
+ type=int,
145
+ default=-1,
146
+ help="illegal parking time which units are seconds, default is -1 which means not recognition illegal parking"
147
+ )
148
+ parser.add_argument(
149
+ "--region_type",
150
+ type=str,
151
+ default='horizontal',
152
+ help="Area type for entrance counting or break in counting, 'horizontal' and "
153
+ "'vertical' used when do entrance counting. 'custom' used when do break in counting. "
154
+ "Note that only support single-class MOT, and the video should be taken by a static camera."
155
+ )
156
+ parser.add_argument(
157
+ '--region_polygon',
158
+ nargs='+',
159
+ type=int,
160
+ default=[],
161
+ help="Clockwise point coords (x0,y0,x1,y1...) of polygon of area when "
162
+ "do_break_in_counting. Note that only support single-class MOT and "
163
+ "the video should be taken by a static camera.")
164
+ parser.add_argument(
165
+ "--secs_interval",
166
+ type=int,
167
+ default=2,
168
+ help="The seconds interval to count after tracking")
169
+ parser.add_argument(
170
+ "--draw_center_traj",
171
+ type=bool,
172
+ default=False,
173
+ help="Whether drawing the trajectory of center")
174
+ parser.add_argument('--avtivity_list', nargs='+', type=str)
175
+
176
+ return parser
177
+
178
+
179
+ def merge_cfg(args):
180
+ # load config
181
+ with open(args.config) as f:
182
+ pred_config = yaml.safe_load(f)
183
+
184
+ def merge(cfg, arg):
185
+ # update cfg from arg directly
186
+ merge_cfg = copy.deepcopy(cfg)
187
+ for k, v in cfg.items():
188
+ if k in arg:
189
+ merge_cfg[k] = arg[k]
190
+ else:
191
+ if isinstance(v, dict):
192
+ merge_cfg[k] = merge(v, arg)
193
+
194
+ return merge_cfg
195
+
196
+ def merge_opt(cfg, arg):
197
+ merge_cfg = copy.deepcopy(cfg)
198
+ # merge opt
199
+ if 'opt' in arg.keys() and arg['opt']:
200
+ for name, value in arg['opt'].items(
201
+ ): # example: {'MOT': {'batch_size': 3}}
202
+ if name not in merge_cfg.keys():
203
+ print("No", name, "in config file!")
204
+ continue
205
+ for sub_k, sub_v in value.items():
206
+ if sub_k not in merge_cfg[name].keys():
207
+ print("No", sub_k, "in config file of", name, "!")
208
+ continue
209
+ merge_cfg[name][sub_k] = sub_v
210
+
211
+ return merge_cfg
212
+
213
+ args_dict = vars(args)
214
+ pred_config = merge(pred_config, args_dict)
215
+ pred_config = merge_opt(pred_config, args_dict)
216
+
217
+ return pred_config
218
+
219
+
220
+ def print_arguments(cfg):
221
+ print('----------- Running Arguments -----------')
222
+ buffer = yaml.dump(cfg)
223
+ print(buffer)
224
+ print('------------------------------------------')
pipeline/config/infer_cfg_pphuman.yml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ crop_thresh: 0.5
2
+ attr_thresh: 0.5
3
+ kpt_thresh: 0.2
4
+ visual: True
5
+ warmup_frame: 50
6
+
7
+ DET:
8
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_l_36e_pipeline.zip
9
+ batch_size: 1
10
+
11
+ MOT:
12
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_l_36e_pipeline.zip
13
+ tracker_config: pipeline/config/tracker_config.yml
14
+ batch_size: 1
15
+ skip_frame_num: -1 # preferably no more than 3
16
+ enable: False
17
+
18
+ KPT:
19
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/dark_hrnet_w32_256x192.zip
20
+ batch_size: 8
21
+
22
+ ATTR:
23
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/PPLCNet_x1_0_person_attribute_945_infer.zip
24
+ batch_size: 8
25
+ enable: False
26
+
27
+ VIDEO_ACTION:
28
+ model_dir: https://videotag.bj.bcebos.com/PaddleVideo-release2.3/ppTSM_fight.zip
29
+ batch_size: 1
30
+ frame_len: 8
31
+ sample_freq: 7
32
+ short_size: 340
33
+ target_size: 320
34
+ enable: False
35
+
36
+ SKELETON_ACTION:
37
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/STGCN.zip
38
+ batch_size: 1
39
+ max_frames: 50
40
+ display_frames: 80
41
+ coord_size: [384, 512]
42
+ enable: False
43
+
44
+ ID_BASED_DETACTION:
45
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/ppyoloe_crn_s_80e_smoking_visdrone.zip
46
+ batch_size: 8
47
+ threshold: 0.6
48
+ display_frames: 80
49
+ skip_frame_num: 2
50
+ enable: False
51
+
52
+ ID_BASED_CLSACTION:
53
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/PPHGNet_tiny_calling_halfbody.zip
54
+ batch_size: 8
55
+ threshold: 0.8
56
+ display_frames: 80
57
+ skip_frame_num: 2
58
+ enable: False
59
+
60
+ REID:
61
+ model_dir: https://bj.bcebos.com/v1/paddledet/models/pipeline/reid_model.zip
62
+ batch_size: 16
63
+ enable: False
pipeline/config/tracker_config.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config of tracker for MOT SDE Detector, use 'JDETracker' as default.
2
+ # The tracker of MOT JDE Detector (such as FairMOT) is exported together with the model.
3
+ # Here 'min_box_area' and 'vertical_ratio' are set for pedestrian, you can modify for other objects tracking.
4
+
5
+ type: OCSORTTracker # choose one tracker in ['JDETracker', 'OCSORTTracker']
6
+
7
+
8
+ # BYTETracker
9
+ JDETracker:
10
+ use_byte: True
11
+ det_thresh: 0.3
12
+ conf_thres: 0.6
13
+ low_conf_thres: 0.1
14
+ match_thres: 0.9
15
+ min_box_area: 0
16
+ vertical_ratio: 0 # 1.6 for pedestrian
17
+
18
+
19
+ OCSORTTracker:
20
+ det_thresh: 0.4
21
+ max_age: 30
22
+ min_hits: 3
23
+ iou_threshold: 0.3
24
+ delta_t: 3
25
+ inertia: 0.2
26
+ vertical_ratio: 0
27
+ min_box_area: 0
28
+ use_byte: False
pipeline/datacollector.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import copy
17
+ from collections import Counter
18
+
19
+
20
+ class Result(object):
21
+ def __init__(self):
22
+ self.res_dict = {
23
+ 'det': dict(),
24
+ 'mot': dict(),
25
+ 'attr': dict(),
26
+ 'kpt': dict(),
27
+ 'video_action': dict(),
28
+ 'skeleton_action': dict(),
29
+ 'reid': dict(),
30
+ 'det_action': dict(),
31
+ 'cls_action': dict(),
32
+ 'vehicleplate': dict(),
33
+ 'vehicle_attr': dict()
34
+ }
35
+
36
+ def update(self, res, name):
37
+ self.res_dict[name].update(res)
38
+
39
+ def get(self, name):
40
+ if name in self.res_dict and len(self.res_dict[name]) > 0:
41
+ return self.res_dict[name]
42
+ return None
43
+
44
+ def clear(self, name):
45
+ self.res_dict[name].clear()
46
+
47
+
48
+ class DataCollector(object):
49
+ """
50
+ DataCollector of Pipeline, collect results in every frames and assign it to each track ids.
51
+ mainly used in mtmct.
52
+
53
+ data struct:
54
+ collector:
55
+ - [id1]: (all results of N frames)
56
+ - frames(list of int): Nx[int]
57
+ - rects(list of rect): Nx[rect(conf, xmin, ymin, xmax, ymax)]
58
+ - features(list of array(256,)): Nx[array(256,)]
59
+ - qualities(list of float): Nx[float]
60
+ - attrs(list of attr): refer to attrs for details
61
+ - kpts(list of kpts): refer to kpts for details
62
+ - skeleton_action(list of skeleton_action): refer to skeleton_action for details
63
+ ...
64
+ - [idN]
65
+ """
66
+
67
+ def __init__(self):
68
+ #id, frame, rect, score, label, attrs, kpts, skeleton_action
69
+ self.mots = {
70
+ "frames": [],
71
+ "rects": [],
72
+ "attrs": [],
73
+ "kpts": [],
74
+ "features": [],
75
+ "qualities": [],
76
+ "skeleton_action": [],
77
+ "vehicleplate": []
78
+ }
79
+ self.collector = {}
80
+
81
+ def append(self, frameid, Result):
82
+ mot_res = Result.get('mot')
83
+ attr_res = Result.get('attr')
84
+ kpt_res = Result.get('kpt')
85
+ skeleton_action_res = Result.get('skeleton_action')
86
+ reid_res = Result.get('reid')
87
+ vehicleplate_res = Result.get('vehicleplate')
88
+
89
+ rects = []
90
+ if reid_res is not None:
91
+ rects = reid_res['rects']
92
+ elif mot_res is not None:
93
+ rects = mot_res['boxes']
94
+
95
+ for idx, mot_item in enumerate(rects):
96
+ ids = int(mot_item[0])
97
+ if ids not in self.collector:
98
+ self.collector[ids] = copy.deepcopy(self.mots)
99
+ self.collector[ids]["frames"].append(frameid)
100
+ self.collector[ids]["rects"].append([mot_item[2:]])
101
+ if attr_res:
102
+ self.collector[ids]["attrs"].append(attr_res['output'][idx])
103
+ if kpt_res:
104
+ self.collector[ids]["kpts"].append([
105
+ kpt_res['keypoint'][0][idx], kpt_res['keypoint'][1][idx]
106
+ ])
107
+ if skeleton_action_res and (idx + 1) in skeleton_action_res:
108
+ self.collector[ids]["skeleton_action"].append(
109
+ skeleton_action_res[idx + 1])
110
+ else:
111
+ # action model generate result per X frames, Not available every frames
112
+ self.collector[ids]["skeleton_action"].append(None)
113
+ if reid_res:
114
+ self.collector[ids]["features"].append(reid_res['features'][
115
+ idx])
116
+ self.collector[ids]["qualities"].append(reid_res['qualities'][
117
+ idx])
118
+ if vehicleplate_res and vehicleplate_res['plate'][idx] != "":
119
+ self.collector[ids]["vehicleplate"].append(vehicleplate_res[
120
+ 'plate'][idx])
121
+
122
+ def get_res(self):
123
+ return self.collector
124
+
125
+ def get_carlp(self, trackid):
126
+ lps = self.collector[trackid]["vehicleplate"]
127
+ counter = Counter(lps)
128
+ carlp = counter.most_common()
129
+ if len(carlp) > 0:
130
+ return carlp[0][0]
131
+ else:
132
+ return None
pipeline/download.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os, sys
16
+ import os.path as osp
17
+ import hashlib
18
+ import requests
19
+ import shutil
20
+ import tqdm
21
+ import time
22
+ import tarfile
23
+ import zipfile
24
+ from paddle.utils.download import _get_unique_endpoints
25
+
26
+ PPDET_WEIGHTS_DOWNLOAD_URL_PREFIX = 'https://paddledet.bj.bcebos.com/'
27
+
28
+ DOWNLOAD_RETRY_LIMIT = 3
29
+
30
+ WEIGHTS_HOME = osp.expanduser("~/.cache/paddle/infer_weights")
31
+
32
+ MODEL_URL_MD5_DICT = {
33
+ 'https://bj.bcebos.com/v1/paddledet/models/pipeline/ch_PP-OCRv3_det_infer.tar.gz':
34
+ '1b8eae0f098635699bd4e8bccf3067a7',
35
+ 'https://bj.bcebos.com/v1/paddledet/models/pipeline/ch_PP-OCRv3_rec_infer.tar.gz':
36
+ '64fa0e0701efd93c7db52a9b685b3de6',
37
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_l_36e_ppvehicle.zip":
38
+ "3859d1a26e0c498285c2374b1a347013",
39
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_s_36e_ppvehicle.zip":
40
+ "4ed58b546be2a76d8ccbb138f64874ac",
41
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/dark_hrnet_w32_256x192.zip":
42
+ "a20d5f6ca087bff0e9f2b18df45a36f2",
43
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/PPLCNet_x1_0_person_attribute_945_infer.zip":
44
+ "1dfb161bf12bbc1365b2ed6866674483",
45
+ "https://videotag.bj.bcebos.com/PaddleVideo-release2.3/ppTSM_fight.zip":
46
+ "5d4609142501258608bf0a1445eedaba",
47
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/STGCN.zip":
48
+ "cf1c3c4bae90b975accb954d13129ea4",
49
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/ppyoloe_crn_s_80e_smoking_visdrone.zip":
50
+ "4cd12ae55be8f0eb2b90c08ac3b48218",
51
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/PPHGNet_tiny_calling_halfbody.zip":
52
+ "cf86b87ace97540dace6ef08e62b584a",
53
+ "https://bj.bcebos.com/v1/paddledet/models/pipeline/reid_model.zip":
54
+ "fdc4dac38393b8e2b5921c1e1fdd5315"
55
+ }
56
+
57
+
58
+ def is_url(path):
59
+ """
60
+ Whether path is URL.
61
+ Args:
62
+ path (string): URL string or not.
63
+ """
64
+ return path.startswith('http://') \
65
+ or path.startswith('https://') \
66
+ or path.startswith('ppdet://')
67
+
68
+
69
+ def parse_url(url):
70
+ url = url.replace("ppdet://", PPDET_WEIGHTS_DOWNLOAD_URL_PREFIX)
71
+ return url
72
+
73
+
74
+ def map_path(url, root_dir, path_depth=1):
75
+ # parse path after download to decompress under root_dir
76
+ assert path_depth > 0, "path_depth should be a positive integer"
77
+ dirname = url
78
+ for _ in range(path_depth):
79
+ dirname = osp.dirname(dirname)
80
+ fpath = osp.relpath(url, dirname)
81
+
82
+ zip_formats = ['.zip', '.tar', '.gz']
83
+ for zip_format in zip_formats:
84
+ fpath = fpath.replace(zip_format, '')
85
+ return osp.join(root_dir, fpath)
86
+
87
+
88
+ def _md5check(fullname, md5sum=None):
89
+ if md5sum is None:
90
+ return True
91
+
92
+ md5 = hashlib.md5()
93
+ with open(fullname, 'rb') as f:
94
+ for chunk in iter(lambda: f.read(4096), b""):
95
+ md5.update(chunk)
96
+ calc_md5sum = md5.hexdigest()
97
+
98
+ if calc_md5sum != md5sum:
99
+ return False
100
+ return True
101
+
102
+
103
+ def _check_exist_file_md5(filename, md5sum, url):
104
+ return _md5check(filename, md5sum)
105
+
106
+
107
+ def _download(url, path, md5sum=None):
108
+ """
109
+ Download from url, save to path.
110
+ url (str): download url
111
+ path (str): download to given path
112
+ """
113
+ if not osp.exists(path):
114
+ os.makedirs(path)
115
+
116
+ fname = osp.split(url)[-1]
117
+ fullname = osp.join(path, fname)
118
+ retry_cnt = 0
119
+ while not (osp.exists(fullname) and _check_exist_file_md5(fullname, md5sum,
120
+ url)):
121
+ if retry_cnt < DOWNLOAD_RETRY_LIMIT:
122
+ retry_cnt += 1
123
+ else:
124
+ raise RuntimeError("Download from {} failed. "
125
+ "Retry limit reached".format(url))
126
+
127
+ # NOTE: windows path join may incur \, which is invalid in url
128
+ if sys.platform == "win32":
129
+ url = url.replace('\\', '/')
130
+
131
+ req = requests.get(url, stream=True)
132
+ if req.status_code != 200:
133
+ raise RuntimeError("Downloading from {} failed with code "
134
+ "{}!".format(url, req.status_code))
135
+
136
+ # For protecting download interupted, download to
137
+ # tmp_fullname firstly, move tmp_fullname to fullname
138
+ # after download finished
139
+ tmp_fullname = fullname + "_tmp"
140
+ total_size = req.headers.get('content-length')
141
+ with open(tmp_fullname, 'wb') as f:
142
+ if total_size:
143
+ for chunk in tqdm.tqdm(
144
+ req.iter_content(chunk_size=1024),
145
+ total=(int(total_size) + 1023) // 1024,
146
+ unit='KB'):
147
+ f.write(chunk)
148
+ else:
149
+ for chunk in req.iter_content(chunk_size=1024):
150
+ if chunk:
151
+ f.write(chunk)
152
+ shutil.move(tmp_fullname, fullname)
153
+ return fullname
154
+
155
+
156
+ def _download_dist(url, path, md5sum=None):
157
+ env = os.environ
158
+ if 'PADDLE_TRAINERS_NUM' in env and 'PADDLE_TRAINER_ID' in env:
159
+ trainer_id = int(env['PADDLE_TRAINER_ID'])
160
+ num_trainers = int(env['PADDLE_TRAINERS_NUM'])
161
+ if num_trainers <= 1:
162
+ return _download(url, path, md5sum)
163
+ else:
164
+ fname = osp.split(url)[-1]
165
+ fullname = osp.join(path, fname)
166
+ lock_path = fullname + '.download.lock'
167
+
168
+ if not osp.isdir(path):
169
+ os.makedirs(path)
170
+
171
+ if not osp.exists(fullname):
172
+ from paddle.distributed import ParallelEnv
173
+ unique_endpoints = _get_unique_endpoints(ParallelEnv()
174
+ .trainer_endpoints[:])
175
+ with open(lock_path, 'w'): # touch
176
+ os.utime(lock_path, None)
177
+ if ParallelEnv().current_endpoint in unique_endpoints:
178
+ _download(url, path, md5sum)
179
+ os.remove(lock_path)
180
+ else:
181
+ while os.path.exists(lock_path):
182
+ time.sleep(0.5)
183
+ return fullname
184
+ else:
185
+ return _download(url, path, md5sum)
186
+
187
+
188
+ def _move_and_merge_tree(src, dst):
189
+ """
190
+ Move src directory to dst, if dst is already exists,
191
+ merge src to dst
192
+ """
193
+ if not osp.exists(dst):
194
+ shutil.move(src, dst)
195
+ elif osp.isfile(src):
196
+ shutil.move(src, dst)
197
+ else:
198
+ for fp in os.listdir(src):
199
+ src_fp = osp.join(src, fp)
200
+ dst_fp = osp.join(dst, fp)
201
+ if osp.isdir(src_fp):
202
+ if osp.isdir(dst_fp):
203
+ _move_and_merge_tree(src_fp, dst_fp)
204
+ else:
205
+ shutil.move(src_fp, dst_fp)
206
+ elif osp.isfile(src_fp) and \
207
+ not osp.isfile(dst_fp):
208
+ shutil.move(src_fp, dst_fp)
209
+
210
+
211
+ def _decompress(fname):
212
+ """
213
+ Decompress for zip and tar file
214
+ """
215
+
216
+ # For protecting decompressing interupted,
217
+ # decompress to fpath_tmp directory firstly, if decompress
218
+ # successed, move decompress files to fpath and delete
219
+ # fpath_tmp and remove download compress file.
220
+ fpath = osp.split(fname)[0]
221
+ fpath_tmp = osp.join(fpath, 'tmp')
222
+ if osp.isdir(fpath_tmp):
223
+ shutil.rmtree(fpath_tmp)
224
+ os.makedirs(fpath_tmp)
225
+
226
+ if fname.find('tar') >= 0:
227
+ with tarfile.open(fname) as tf:
228
+ tf.extractall(path=fpath_tmp)
229
+ elif fname.find('zip') >= 0:
230
+ with zipfile.ZipFile(fname) as zf:
231
+ zf.extractall(path=fpath_tmp)
232
+ elif fname.find('.txt') >= 0:
233
+ return
234
+ else:
235
+ raise TypeError("Unsupport compress file type {}".format(fname))
236
+
237
+ for f in os.listdir(fpath_tmp):
238
+ src_dir = osp.join(fpath_tmp, f)
239
+ dst_dir = osp.join(fpath, f)
240
+ _move_and_merge_tree(src_dir, dst_dir)
241
+
242
+ shutil.rmtree(fpath_tmp)
243
+ os.remove(fname)
244
+
245
+
246
+ def _decompress_dist(fname):
247
+ env = os.environ
248
+ if 'PADDLE_TRAINERS_NUM' in env and 'PADDLE_TRAINER_ID' in env:
249
+ trainer_id = int(env['PADDLE_TRAINER_ID'])
250
+ num_trainers = int(env['PADDLE_TRAINERS_NUM'])
251
+ if num_trainers <= 1:
252
+ _decompress(fname)
253
+ else:
254
+ lock_path = fname + '.decompress.lock'
255
+ from paddle.distributed import ParallelEnv
256
+ unique_endpoints = _get_unique_endpoints(ParallelEnv()
257
+ .trainer_endpoints[:])
258
+ # NOTE(dkp): _decompress_dist always performed after
259
+ # _download_dist, in _download_dist sub-trainers is waiting
260
+ # for download lock file release with sleeping, if decompress
261
+ # prograss is very fast and finished with in the sleeping gap
262
+ # time, e.g in tiny dataset such as coco_ce, spine_coco, main
263
+ # trainer may finish decompress and release lock file, so we
264
+ # only craete lock file in main trainer and all sub-trainer
265
+ # wait 1s for main trainer to create lock file, for 1s is
266
+ # twice as sleeping gap, this waiting time can keep all
267
+ # trainer pipeline in order
268
+ # **change this if you have more elegent methods**
269
+ if ParallelEnv().current_endpoint in unique_endpoints:
270
+ with open(lock_path, 'w'): # touch
271
+ os.utime(lock_path, None)
272
+ _decompress(fname)
273
+ os.remove(lock_path)
274
+ else:
275
+ time.sleep(1)
276
+ while os.path.exists(lock_path):
277
+ time.sleep(0.5)
278
+ else:
279
+ _decompress(fname)
280
+
281
+
282
+ def get_path(url, root_dir=WEIGHTS_HOME, md5sum=None, check_exist=True):
283
+ """ Download from given url to root_dir.
284
+ if file or directory specified by url is exists under
285
+ root_dir, return the path directly, otherwise download
286
+ from url and decompress it, return the path.
287
+ url (str): download url
288
+ root_dir (str): root dir for downloading
289
+ md5sum (str): md5 sum of download package
290
+ """
291
+ # parse path after download to decompress under root_dir
292
+ fullpath = map_path(url, root_dir)
293
+
294
+ # For same zip file, decompressed directory name different
295
+ # from zip file name, rename by following map
296
+ decompress_name_map = {"ppTSM_fight": "ppTSM", }
297
+ for k, v in decompress_name_map.items():
298
+ if fullpath.find(k) >= 0:
299
+ fullpath = osp.join(osp.split(fullpath)[0], v)
300
+
301
+ if osp.exists(fullpath) and check_exist:
302
+ if not osp.isfile(fullpath) or \
303
+ _check_exist_file_md5(fullpath, md5sum, url):
304
+ return fullpath, True
305
+ else:
306
+ os.remove(fullpath)
307
+
308
+ fullname = _download_dist(url, root_dir, md5sum)
309
+
310
+ # new weights format which postfix is 'pdparams' not
311
+ # need to decompress
312
+ if osp.splitext(fullname)[-1] not in ['.pdparams', '.yml']:
313
+ _decompress_dist(fullname)
314
+
315
+ return fullpath, False
316
+
317
+
318
+ def get_weights_path(url):
319
+ """Get weights path from WEIGHTS_HOME, if not exists,
320
+ download it from url.
321
+ """
322
+ url = parse_url(url)
323
+ md5sum = None
324
+ if url in MODEL_URL_MD5_DICT.keys():
325
+ md5sum = MODEL_URL_MD5_DICT[url]
326
+ path, _ = get_path(url, WEIGHTS_HOME, md5sum)
327
+ return path
328
+
329
+
330
+ def auto_download_model(model_path):
331
+ # auto download
332
+ if is_url(model_path):
333
+ weight = get_weights_path(model_path)
334
+ return weight
335
+ return None
336
+
337
+
338
+ if __name__ == "__main__":
339
+ model_path = "https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_l_36e_pipeline.zip"
340
+ auto_download_model(model_path)
pipeline/pipe_utils.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import time
16
+ import os
17
+ import ast
18
+ import glob
19
+ import yaml
20
+ import copy
21
+ import numpy as np
22
+ import subprocess as sp
23
+
24
+ from python.keypoint_preprocess import EvalAffine, TopDownEvalAffine, expand_crop
25
+
26
+
27
+ class Times(object):
28
+ def __init__(self):
29
+ self.time = 0.
30
+ # start time
31
+ self.st = 0.
32
+ # end time
33
+ self.et = 0.
34
+
35
+ def start(self):
36
+ self.st = time.time()
37
+
38
+ def end(self, repeats=1, accumulative=True):
39
+ self.et = time.time()
40
+ if accumulative:
41
+ self.time += (self.et - self.st) / repeats
42
+ else:
43
+ self.time = (self.et - self.st) / repeats
44
+
45
+ def reset(self):
46
+ self.time = 0.
47
+ self.st = 0.
48
+ self.et = 0.
49
+
50
+ def value(self):
51
+ return round(self.time, 4)
52
+
53
+
54
+ class PipeTimer(Times):
55
+ def __init__(self):
56
+ super(PipeTimer, self).__init__()
57
+ self.total_time = Times()
58
+ self.module_time = {
59
+ 'det': Times(),
60
+ 'mot': Times(),
61
+ 'attr': Times(),
62
+ 'kpt': Times(),
63
+ 'video_action': Times(),
64
+ 'skeleton_action': Times(),
65
+ 'reid': Times(),
66
+ 'det_action': Times(),
67
+ 'cls_action': Times(),
68
+ 'vehicle_attr': Times(),
69
+ 'vehicleplate': Times()
70
+ }
71
+ self.img_num = 0
72
+ self.track_num = 0
73
+
74
+ def get_total_time(self):
75
+ total_time = self.total_time.value()
76
+ total_time = round(total_time, 4)
77
+ average_latency = total_time / max(1, self.img_num)
78
+ qps = 0
79
+ if total_time > 0:
80
+ qps = 1 / average_latency
81
+ return total_time, average_latency, qps
82
+
83
+ def info(self):
84
+ total_time, average_latency, qps = self.get_total_time()
85
+ print("------------------ Inference Time Info ----------------------")
86
+ print("total_time(ms): {}, img_num: {}".format(total_time * 1000,
87
+ self.img_num))
88
+
89
+ for k, v in self.module_time.items():
90
+ v_time = round(v.value(), 4)
91
+ if v_time > 0 and k in ['det', 'mot', 'video_action']:
92
+ print("{} time(ms): {}; per frame average time(ms): {}".format(
93
+ k, v_time * 1000, v_time * 1000 / self.img_num))
94
+ elif v_time > 0:
95
+ print("{} time(ms): {}; per trackid average time(ms): {}".
96
+ format(k, v_time * 1000, v_time * 1000 / self.track_num))
97
+
98
+ print("average latency time(ms): {:.2f}, QPS: {:2f}".format(
99
+ average_latency * 1000, qps))
100
+ return qps
101
+
102
+ def report(self, average=False):
103
+ dic = {}
104
+ dic['total'] = round(self.total_time.value() / max(1, self.img_num),
105
+ 4) if average else self.total_time.value()
106
+ dic['det'] = round(self.module_time['det'].value() /
107
+ max(1, self.img_num),
108
+ 4) if average else self.module_time['det'].value()
109
+ dic['mot'] = round(self.module_time['mot'].value() /
110
+ max(1, self.img_num),
111
+ 4) if average else self.module_time['mot'].value()
112
+ dic['attr'] = round(self.module_time['attr'].value() /
113
+ max(1, self.img_num),
114
+ 4) if average else self.module_time['attr'].value()
115
+ dic['kpt'] = round(self.module_time['kpt'].value() /
116
+ max(1, self.img_num),
117
+ 4) if average else self.module_time['kpt'].value()
118
+ dic['video_action'] = self.module_time['video_action'].value()
119
+ dic['skeleton_action'] = round(
120
+ self.module_time['skeleton_action'].value() / max(1, self.img_num),
121
+ 4) if average else self.module_time['skeleton_action'].value()
122
+
123
+ dic['img_num'] = self.img_num
124
+ return dic
125
+
126
+
127
+ class PushStream(object):
128
+ def __init__(self, pushurl="rtsp://127.0.0.1:8554/"):
129
+ self.command = ""
130
+ # 自行设置
131
+ self.pushurl = pushurl
132
+
133
+ def initcmd(self, fps, width, height):
134
+ self.command = [
135
+ 'ffmpeg', '-y', '-f', 'rawvideo', '-vcodec', 'rawvideo',
136
+ '-pix_fmt', 'bgr24', '-s', "{}x{}".format(width, height), '-r',
137
+ str(fps), '-i', '-', '-pix_fmt', 'yuv420p', '-f', 'rtsp',
138
+ self.pushurl
139
+ ]
140
+ self.pipe = sp.Popen(self.command, stdin=sp.PIPE)
141
+
142
+
143
+ def get_test_images(infer_dir, infer_img):
144
+ """
145
+ Get image path list in TEST mode
146
+ """
147
+ assert infer_img is not None or infer_dir is not None, \
148
+ "--infer_img or --infer_dir should be set"
149
+ assert infer_img is None or os.path.isfile(infer_img), \
150
+ "{} is not a file".format(infer_img)
151
+ assert infer_dir is None or os.path.isdir(infer_dir), \
152
+ "{} is not a directory".format(infer_dir)
153
+
154
+ # infer_img has a higher priority
155
+ if infer_img and os.path.isfile(infer_img):
156
+ return [infer_img]
157
+
158
+ images = set()
159
+ infer_dir = os.path.abspath(infer_dir)
160
+ assert os.path.isdir(infer_dir), \
161
+ "infer_dir {} is not a directory".format(infer_dir)
162
+ exts = ['jpg', 'jpeg', 'png', 'bmp']
163
+ exts += [ext.upper() for ext in exts]
164
+ for ext in exts:
165
+ images.update(glob.glob('{}/*.{}'.format(infer_dir, ext)))
166
+ images = list(images)
167
+
168
+ assert len(images) > 0, "no image found in {}".format(infer_dir)
169
+ print("Found {} inference images in total.".format(len(images)))
170
+
171
+ return images
172
+
173
+
174
+ def crop_image_with_det(batch_input, det_res, thresh=0.3):
175
+ boxes = det_res['boxes']
176
+ score = det_res['boxes'][:, 1]
177
+ boxes_num = det_res['boxes_num']
178
+ start_idx = 0
179
+ crop_res = []
180
+ for b_id, input in enumerate(batch_input):
181
+ boxes_num_i = boxes_num[b_id]
182
+ if boxes_num_i == 0:
183
+ continue
184
+ boxes_i = boxes[start_idx:start_idx + boxes_num_i, :]
185
+ score_i = score[start_idx:start_idx + boxes_num_i]
186
+ res = []
187
+ for box, s in zip(boxes_i, score_i):
188
+ if s > thresh:
189
+ crop_image, new_box, ori_box = expand_crop(input, box)
190
+ if crop_image is not None:
191
+ res.append(crop_image)
192
+ crop_res.append(res)
193
+ return crop_res
194
+
195
+
196
+ def normal_crop(image, rect):
197
+ imgh, imgw, c = image.shape
198
+ label, conf, xmin, ymin, xmax, ymax = [int(x) for x in rect.tolist()]
199
+ org_rect = [xmin, ymin, xmax, ymax]
200
+ if label != 0:
201
+ return None, None, None
202
+ xmin = max(0, xmin)
203
+ ymin = max(0, ymin)
204
+ xmax = min(imgw, xmax)
205
+ ymax = min(imgh, ymax)
206
+ return image[ymin:ymax, xmin:xmax, :], [xmin, ymin, xmax, ymax], org_rect
207
+
208
+
209
+ def crop_image_with_mot(input, mot_res, expand=True):
210
+ res = mot_res['boxes']
211
+ crop_res = []
212
+ new_bboxes = []
213
+ ori_bboxes = []
214
+ for box in res:
215
+ if expand:
216
+ crop_image, new_bbox, ori_bbox = expand_crop(input, box[1:])
217
+ else:
218
+ crop_image, new_bbox, ori_bbox = normal_crop(input, box[1:])
219
+ if crop_image is not None:
220
+ crop_res.append(crop_image)
221
+ new_bboxes.append(new_bbox)
222
+ ori_bboxes.append(ori_bbox)
223
+ return crop_res, new_bboxes, ori_bboxes
224
+
225
+
226
+ def parse_mot_res(input):
227
+ mot_res = []
228
+ boxes, scores, ids = input[0]
229
+ for box, score, i in zip(boxes[0], scores[0], ids[0]):
230
+ xmin, ymin, w, h = box
231
+ res = [i, 0, score, xmin, ymin, xmin + w, ymin + h]
232
+ mot_res.append(res)
233
+ return {'boxes': np.array(mot_res)}
234
+
235
+
236
+ def refine_keypoint_coordinary(kpts, bbox, coord_size):
237
+ """
238
+ This function is used to adjust coordinate values to a fixed scale.
239
+ """
240
+ tl = bbox[:, 0:2]
241
+ wh = bbox[:, 2:] - tl
242
+ tl = np.expand_dims(np.transpose(tl, (1, 0)), (2, 3))
243
+ wh = np.expand_dims(np.transpose(wh, (1, 0)), (2, 3))
244
+ target_w, target_h = coord_size
245
+ res = (kpts - tl) / wh * np.expand_dims(
246
+ np.array([[target_w], [target_h]]), (2, 3))
247
+ return res
248
+
249
+
250
+ def parse_mot_keypoint(input, coord_size):
251
+ parsed_skeleton_with_mot = {}
252
+ ids = []
253
+ skeleton = []
254
+ for tracker_id, kpt_seq in input:
255
+ ids.append(tracker_id)
256
+ kpts = np.array(kpt_seq.kpts, dtype=np.float32)[:, :, :2]
257
+ kpts = np.expand_dims(np.transpose(kpts, [2, 0, 1]),
258
+ -1) #T, K, C -> C, T, K, 1
259
+ bbox = np.array(kpt_seq.bboxes, dtype=np.float32)
260
+ skeleton.append(refine_keypoint_coordinary(kpts, bbox, coord_size))
261
+ parsed_skeleton_with_mot["mot_id"] = ids
262
+ parsed_skeleton_with_mot["skeleton"] = skeleton
263
+ return parsed_skeleton_with_mot
pipeline/pipeline.py ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+ import cv2
19
+ import numpy as np
20
+ import math
21
+ import paddle
22
+ import sys
23
+ import copy
24
+ from collections import defaultdict
25
+ from pipeline.datacollector import DataCollector, Result
26
+
27
+ # add deploy path of PadleDetection to sys.path
28
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2)))
29
+ sys.path.insert(0, parent_path)
30
+
31
+ from pipeline.cfg_utils import argsparser, merge_cfg
32
+ from pipeline.pipe_utils import get_test_images, crop_image_with_det, crop_image_with_mot, parse_mot_res, parse_mot_keypoint
33
+ from pipeline.pipe_utils import PushStream, PipeTimer
34
+
35
+ from python.infer import Detector, DetectorPicoDet
36
+ from python.keypoint_infer import KeyPointDetector
37
+ from python.keypoint_postprocess import translate_to_ori_images
38
+ from python.preprocess import decode_image, ShortSizeScale
39
+ from python.visualize import visualize_box_mask, visualize_attr, visualize_pose, visualize_action, visualize_vehicleplate
40
+
41
+ from pptracking.python.mot_sde_infer import SDE_Detector
42
+ from pptracking.python.mot.visualize import plot_tracking_dict
43
+ from pptracking.python.mot.utils import flow_statistic, update_object_info
44
+
45
+ from pipeline.pphuman.attr_infer import AttrDetector
46
+ from pipeline.pphuman.video_action_infer import VideoActionRecognizer
47
+ from pipeline.pphuman.action_infer import SkeletonActionRecognizer, DetActionRecognizer, ClsActionRecognizer
48
+ from pipeline.pphuman.action_utils import KeyPointBuff, ActionVisualHelper
49
+ from pipeline.pphuman.reid import ReID
50
+ from pipeline.pphuman.mtmct import mtmct_process
51
+
52
+ from pipeline.download import auto_download_model
53
+
54
+
55
+ class Pipeline(object):
56
+ """
57
+ Pipeline
58
+
59
+ Args:
60
+ args (argparse.Namespace): arguments in pipeline, which contains environment and runtime settings
61
+ cfg (dict): config of models in pipeline
62
+ """
63
+
64
+ def __init__(self, args, cfg):
65
+ self.multi_camera = False
66
+ reid_cfg = cfg.get('REID', False)
67
+ self.enable_mtmct = reid_cfg['enable'] if reid_cfg else False
68
+ self.is_video = False
69
+ self.output_dir = args.output_dir
70
+ self.vis_result = cfg['visual']
71
+ self.input = self._parse_input(args.image_file, args.video_file)
72
+
73
+ self.predictor = PipePredictor(args, cfg, self.is_video)
74
+ if self.is_video:
75
+ self.predictor.set_file_name(self.input)
76
+
77
+ def _parse_input(self, image, video_file):
78
+
79
+ # parse input as is_video and multi_camera
80
+
81
+ if image is not None:
82
+
83
+ input = image
84
+ self.is_video = False
85
+ self.multi_camera = False
86
+
87
+ elif video_file is not None:
88
+ input = video_file
89
+ self.is_video = True
90
+ else:
91
+ raise ValueError(
92
+ "Illegal Input, please set one of ['video_file', 'camera_id', 'image_file', 'image_dir']"
93
+ )
94
+
95
+ return input
96
+
97
+ def run_multithreads(self):
98
+
99
+ out = self.predictor.run(self.input)
100
+ return out
101
+
102
+ def run(self):
103
+ out = self.predictor.run(self.input)
104
+ return out
105
+
106
+
107
+ def get_model_dir_with_list(cfg, args):
108
+ activate_list = args.avtivity_list
109
+ """
110
+ Auto download inference model if the model_path is a url link.
111
+ Otherwise it will use the model_path directly.
112
+ """
113
+ for key in cfg.keys():
114
+ if type(cfg[key]) == dict and ((key in activate_list) or
115
+ "enable" not in cfg[key].keys()):
116
+ if "model_dir" in cfg[key].keys():
117
+ model_dir = cfg[key]["model_dir"]
118
+ downloaded_model_dir = auto_download_model(model_dir)
119
+ if downloaded_model_dir:
120
+ model_dir = downloaded_model_dir
121
+ cfg[key]["model_dir"] = model_dir
122
+ print(key, " model dir: ", model_dir)
123
+ elif key == "VEHICLE_PLATE":
124
+ det_model_dir = cfg[key]["det_model_dir"]
125
+ downloaded_det_model_dir = auto_download_model(det_model_dir)
126
+ if downloaded_det_model_dir:
127
+ det_model_dir = downloaded_det_model_dir
128
+ cfg[key]["det_model_dir"] = det_model_dir
129
+ print("det_model_dir model dir: ", det_model_dir)
130
+
131
+ rec_model_dir = cfg[key]["rec_model_dir"]
132
+ downloaded_rec_model_dir = auto_download_model(rec_model_dir)
133
+ if downloaded_rec_model_dir:
134
+ rec_model_dir = downloaded_rec_model_dir
135
+ cfg[key]["rec_model_dir"] = rec_model_dir
136
+ print("rec_model_dir model dir: ", rec_model_dir)
137
+
138
+ if (key == 'ID_BASED_DETACTION' and (key in activate_list)) or (key == 'SKELETON_ACTION' and (key in activate_list))or (key == 'ATTR' and (key in activate_list)) or (key =='ID_BASED_CLSACTION' and (key in activate_list)) or (key=='REID' and (key in activate_list)):
139
+ model_dir = cfg['MOT']["model_dir"]
140
+ downloaded_model_dir = auto_download_model(model_dir)
141
+ if downloaded_model_dir:
142
+ model_dir = downloaded_model_dir
143
+ cfg['MOT']["model_dir"] = model_dir
144
+ print("mot_model_dir model_dir: ", model_dir)
145
+
146
+
147
+ def get_model_dir(cfg):
148
+ """
149
+ Auto download inference model if the model_path is a url link.
150
+ Otherwise it will use the model_path directly.
151
+ """
152
+ for key in cfg.keys():
153
+ if type(cfg[key]) == dict and \
154
+ ("enable" in cfg[key].keys() and cfg[key]['enable']
155
+ or "enable" not in cfg[key].keys()):
156
+
157
+ if "model_dir" in cfg[key].keys():
158
+ model_dir = cfg[key]["model_dir"]
159
+ downloaded_model_dir = auto_download_model(model_dir)
160
+ if downloaded_model_dir:
161
+ model_dir = downloaded_model_dir
162
+ cfg[key]["model_dir"] = model_dir
163
+ print(key, " model dir: ", model_dir)
164
+ elif key == "VEHICLE_PLATE":
165
+ det_model_dir = cfg[key]["det_model_dir"]
166
+ downloaded_det_model_dir = auto_download_model(det_model_dir)
167
+ if downloaded_det_model_dir:
168
+ det_model_dir = downloaded_det_model_dir
169
+ cfg[key]["det_model_dir"] = det_model_dir
170
+ print("det_model_dir model dir: ", det_model_dir)
171
+
172
+ rec_model_dir = cfg[key]["rec_model_dir"]
173
+ downloaded_rec_model_dir = auto_download_model(rec_model_dir)
174
+ if downloaded_rec_model_dir:
175
+ rec_model_dir = downloaded_rec_model_dir
176
+ cfg[key]["rec_model_dir"] = rec_model_dir
177
+ print("rec_model_dir model dir: ", rec_model_dir)
178
+
179
+ elif key == "MOT": # for idbased and skeletonbased actions
180
+ model_dir = cfg[key]["model_dir"]
181
+ downloaded_model_dir = auto_download_model(model_dir)
182
+ if downloaded_model_dir:
183
+ model_dir = downloaded_model_dir
184
+ cfg[key]["model_dir"] = model_dir
185
+ print("mot_model_dir model_dir: ", model_dir)
186
+
187
+
188
+ class PipePredictor(object):
189
+ """
190
+ Predictor in single camera
191
+
192
+ The pipeline for image input:
193
+
194
+ 1. Detection
195
+ 2. Detection -> Attribute
196
+
197
+ The pipeline for video input:
198
+
199
+ 1. Tracking
200
+ 2. Tracking -> Attribute
201
+ 3. Tracking -> KeyPoint -> SkeletonAction Recognition
202
+ 4. VideoAction Recognition
203
+
204
+ Args:
205
+ args (argparse.Namespace): arguments in pipeline, which contains environment and runtime settings
206
+ cfg (dict): config of models in pipeline
207
+ is_video (bool): whether the input is video, default as False
208
+ multi_camera (bool): whether to use multi camera in pipeline,
209
+ default as False
210
+ """
211
+
212
+ def __init__(self, args, cfg, is_video=True, multi_camera=False):
213
+ # general module for pphuman and ppvehicle
214
+ activate_list = args.avtivity_list
215
+ self.with_mot = True if 'MOT' in activate_list else False
216
+ self.with_human_attr = True if 'ATTR' in activate_list else False
217
+ if self.with_mot:
218
+ print('Multi-Object Tracking enabled')
219
+ if self.with_human_attr:
220
+ print('Human Attribute Recognition enabled')
221
+
222
+ # only for pphuman
223
+ self.with_skeleton_action = True if 'SKELETON_ACTION' in activate_list else False
224
+
225
+ self.with_video_action = True if 'VIDEO_ACTION' in activate_list else False
226
+
227
+ self.with_idbased_detaction = True if 'ID_BASED_DETACTION' in activate_list else False
228
+
229
+ self.with_idbased_clsaction = True if 'ID_BASED_CLSACTION' in activate_list else False
230
+
231
+ self.with_mtmct = True if 'REID' in activate_list else False
232
+
233
+ if self.with_skeleton_action:
234
+ print('SkeletonAction Recognition enabled')
235
+ if self.with_video_action:
236
+ print('VideoAction Recognition enabled')
237
+ if self.with_idbased_detaction:
238
+ print('IDBASED Detection Action Recognition enabled')
239
+ if self.with_idbased_clsaction:
240
+ print('IDBASED Classification Action Recognition enabled')
241
+ if self.with_mtmct:
242
+ print("MTMCT enabled")
243
+
244
+ self.modebase = {
245
+ "framebased": False,
246
+ "videobased": False,
247
+ "idbased": False,
248
+ "skeletonbased": False
249
+ }
250
+
251
+ self.basemode = {
252
+ "MOT": "idbased",
253
+ "ATTR": "idbased",
254
+ "VIDEO_ACTION": "videobased",
255
+ "SKELETON_ACTION": "skeletonbased",
256
+ "ID_BASED_DETACTION": "idbased",
257
+ "ID_BASED_CLSACTION": "idbased",
258
+ "REID": "idbased",
259
+ }
260
+
261
+ self.is_video = is_video
262
+ self.multi_camera = multi_camera
263
+ self.cfg = cfg
264
+
265
+ self.output_dir = args.output_dir
266
+ self.draw_center_traj = True if 'draw_center_traj' in activate_list else False
267
+ self.secs_interval = args.secs_interval
268
+ self.do_entrance_counting = True if 'do_entrance_counting' in activate_list else False
269
+ self.do_break_in_counting = args.do_break_in_counting
270
+ self.region_type = args.region_type
271
+ self.region_polygon = args.region_polygon
272
+ self.illegal_parking_time = args.illegal_parking_time
273
+
274
+ self.warmup_frame = self.cfg['warmup_frame']
275
+ self.pipeline_res = Result()
276
+ self.pipe_timer = PipeTimer()
277
+ self.file_name = None
278
+ self.collector = DataCollector()
279
+
280
+ self.pushurl = args.pushurl
281
+
282
+ # auto download inference model
283
+ get_model_dir_with_list(self.cfg, args)
284
+
285
+ if self.with_human_attr:
286
+ attr_cfg = self.cfg['ATTR']
287
+ basemode = self.basemode['ATTR']
288
+ self.modebase[basemode] = True
289
+ self.attr_predictor = AttrDetector.init_with_cfg(args, attr_cfg)
290
+
291
+ if not is_video:
292
+ det_cfg = self.cfg['DET']
293
+ model_dir = det_cfg['model_dir']
294
+ batch_size = det_cfg['batch_size']
295
+ self.det_predictor = Detector(
296
+ model_dir, args.device, args.run_mode, batch_size,
297
+ args.trt_min_shape, args.trt_max_shape, args.trt_opt_shape,
298
+ args.trt_calib_mode, args.cpu_threads, args.enable_mkldnn)
299
+ else:
300
+ if self.with_idbased_detaction:
301
+ idbased_detaction_cfg = self.cfg['ID_BASED_DETACTION']
302
+ basemode = self.basemode['ID_BASED_DETACTION']
303
+ self.modebase[basemode] = True
304
+
305
+ self.det_action_predictor = DetActionRecognizer.init_with_cfg(
306
+ args, idbased_detaction_cfg)
307
+ self.det_action_visual_helper = ActionVisualHelper(1)
308
+
309
+ if self.with_idbased_clsaction:
310
+ idbased_clsaction_cfg = self.cfg['ID_BASED_CLSACTION']
311
+ basemode = self.basemode['ID_BASED_CLSACTION']
312
+ self.modebase[basemode] = True
313
+
314
+ self.cls_action_predictor = ClsActionRecognizer.init_with_cfg(
315
+ args, idbased_clsaction_cfg)
316
+ self.cls_action_visual_helper = ActionVisualHelper(1)
317
+
318
+ if self.with_skeleton_action:
319
+ skeleton_action_cfg = self.cfg['SKELETON_ACTION']
320
+ display_frames = skeleton_action_cfg['display_frames']
321
+ self.coord_size = skeleton_action_cfg['coord_size']
322
+ basemode = self.basemode['SKELETON_ACTION']
323
+ self.modebase[basemode] = True
324
+ skeleton_action_frames = skeleton_action_cfg['max_frames']
325
+
326
+ self.skeleton_action_predictor = SkeletonActionRecognizer.init_with_cfg(
327
+ args, skeleton_action_cfg)
328
+ self.skeleton_action_visual_helper = ActionVisualHelper(
329
+ display_frames)
330
+
331
+ kpt_cfg = self.cfg['KPT']
332
+ kpt_model_dir = kpt_cfg['model_dir']
333
+ kpt_batch_size = kpt_cfg['batch_size']
334
+ self.kpt_predictor = KeyPointDetector(
335
+ kpt_model_dir,
336
+ args.device,
337
+ args.run_mode,
338
+ kpt_batch_size,
339
+ args.trt_min_shape,
340
+ args.trt_max_shape,
341
+ args.trt_opt_shape,
342
+ args.trt_calib_mode,
343
+ args.cpu_threads,
344
+ args.enable_mkldnn,
345
+ use_dark=False)
346
+ self.kpt_buff = KeyPointBuff(skeleton_action_frames)
347
+
348
+ if self.with_mtmct:
349
+ reid_cfg = self.cfg['REID']
350
+ basemode = self.basemode['REID']
351
+ self.modebase[basemode] = True
352
+ self.reid_predictor = ReID.init_with_cfg(args, reid_cfg)
353
+
354
+ if self.with_mot or self.modebase["idbased"] or self.modebase[
355
+ "skeletonbased"]:
356
+ mot_cfg = self.cfg['MOT']
357
+ model_dir = mot_cfg['model_dir']
358
+ tracker_config = mot_cfg['tracker_config']
359
+ batch_size = mot_cfg['batch_size']
360
+ skip_frame_num = mot_cfg.get('skip_frame_num', -1)
361
+ basemode = self.basemode['MOT']
362
+ self.modebase[basemode] = True
363
+ self.mot_predictor = SDE_Detector(
364
+ model_dir,
365
+ tracker_config,
366
+ args.device,
367
+ args.run_mode,
368
+ batch_size,
369
+ args.trt_min_shape,
370
+ args.trt_max_shape,
371
+ args.trt_opt_shape,
372
+ args.trt_calib_mode,
373
+ args.cpu_threads,
374
+ args.enable_mkldnn,
375
+ skip_frame_num=skip_frame_num,
376
+ draw_center_traj=self.draw_center_traj,
377
+ secs_interval=self.secs_interval,
378
+ do_entrance_counting=self.do_entrance_counting,
379
+ do_break_in_counting=self.do_break_in_counting,
380
+ region_type=self.region_type,
381
+ region_polygon=self.region_polygon)
382
+
383
+ if self.with_video_action:
384
+ video_action_cfg = self.cfg['VIDEO_ACTION']
385
+ basemode = self.basemode['VIDEO_ACTION']
386
+ self.modebase[basemode] = True
387
+ self.video_action_predictor = VideoActionRecognizer.init_with_cfg(
388
+ args, video_action_cfg)
389
+
390
+ def set_file_name(self, path):
391
+ if path is not None:
392
+ if "." in path:
393
+ self.file_name = path.split(".")[-2]
394
+ else:
395
+ # use camera id
396
+ self.file_name = None
397
+
398
+ def get_result(self):
399
+ return self.collector.get_res()
400
+
401
+ def run(self, input, thread_idx=0):
402
+
403
+ if self.is_video:
404
+ out = self.predict_video(input, thread_idx=thread_idx)
405
+ return out
406
+ else:
407
+ out = self.predict_image(input)
408
+ return out
409
+
410
+ def predict_image(self, input):
411
+ # det
412
+ # det -> attr
413
+ batch_input = [decode_image(input, {})[0]]
414
+ batch_input[0] = cv2.cvtColor(batch_input[0], cv2.COLOR_BGR2RGB)
415
+ # det output format: class, score, xmin, ymin, xmax, ymax
416
+ det_res = self.det_predictor.predict_image(batch_input, visual=False)
417
+ det_res = self.det_predictor.filter_box(det_res,
418
+ self.cfg['crop_thresh'])
419
+
420
+ self.pipeline_res.update(det_res, 'det')
421
+
422
+ if self.with_human_attr:
423
+ crop_inputs = crop_image_with_det(batch_input, det_res)
424
+ attr_res_list = []
425
+
426
+ for crop_input in crop_inputs:
427
+ attr_res = self.attr_predictor.predict_image(
428
+ crop_input, visual=False)
429
+ attr_res_list.extend(attr_res['output'])
430
+
431
+ attr_res = {'output': attr_res_list}
432
+ self.pipeline_res.update(attr_res, 'attr')
433
+
434
+ return self.visualize_image(batch_input, self.pipeline_res)
435
+
436
+ def predict_video(self, video_file, thread_idx=0):
437
+ # mot
438
+ # mot -> attr
439
+ # mot -> pose -> action
440
+ capture = cv2.VideoCapture(video_file)
441
+
442
+ # Get Video info : resolution, fps, frame count
443
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
444
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
445
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
446
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
447
+ print("video fps: %d, frame_count: %d" % (fps, frame_count))
448
+
449
+ video_out_name = 'output' if self.file_name is None else self.file_name
450
+ out_path = video_out_name + "_output.mp4"
451
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
452
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
453
+
454
+ frame_id = 0
455
+
456
+ entrance, records, center_traj = None, None, None
457
+ if self.draw_center_traj:
458
+ center_traj = [{}]
459
+ id_set = set()
460
+ interval_id_set = set()
461
+ in_id_list = list()
462
+ out_id_list = list()
463
+ prev_center = dict()
464
+ records = list()
465
+ if self.do_entrance_counting or self.do_break_in_counting or self.illegal_parking_time != -1:
466
+ if self.region_type == 'horizontal':
467
+ entrance = [0, height / 2., width, height / 2.]
468
+ elif self.region_type == 'vertical':
469
+ entrance = [width / 2, 0., width / 2, height]
470
+ elif self.region_type == 'custom':
471
+ entrance = []
472
+ assert len(
473
+ self.region_polygon
474
+ ) % 2 == 0, "region_polygon should be pairs of coords points when do break_in counting."
475
+ assert len(
476
+ self.region_polygon
477
+ ) > 6, 'region_type is custom, region_polygon should be at least 3 pairs of point coords.'
478
+
479
+ for i in range(0, len(self.region_polygon), 2):
480
+ entrance.append(
481
+ [self.region_polygon[i], self.region_polygon[i + 1]])
482
+ entrance.append([width, height])
483
+ else:
484
+ raise ValueError("region_type:{} unsupported.".format(
485
+ self.region_type))
486
+
487
+ video_fps = fps
488
+
489
+ video_action_imgs = []
490
+
491
+ if self.with_video_action:
492
+ short_size = self.cfg["VIDEO_ACTION"]["short_size"]
493
+ scale = ShortSizeScale(short_size)
494
+
495
+ object_in_region_info = {
496
+ } # store info for vehicle parking in region
497
+ illegal_parking_dict = None
498
+
499
+ while (1):
500
+ if frame_id % 10 == 0:
501
+ print('Thread: {}; frame id: {}'.format(thread_idx, frame_id))
502
+
503
+ ret, frame = capture.read()
504
+ if not ret:
505
+ break
506
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
507
+ if frame_id > self.warmup_frame:
508
+ self.pipe_timer.total_time.start()
509
+
510
+ if self.modebase["idbased"] or self.modebase["skeletonbased"]:
511
+ if frame_id > self.warmup_frame:
512
+ self.pipe_timer.module_time['mot'].start()
513
+
514
+ mot_skip_frame_num = self.mot_predictor.skip_frame_num
515
+ reuse_det_result = False
516
+ if mot_skip_frame_num > 1 and frame_id > 0 and frame_id % mot_skip_frame_num > 0:
517
+ reuse_det_result = True
518
+ res = self.mot_predictor.predict_image(
519
+ [copy.deepcopy(frame_rgb)],
520
+ visual=False,
521
+ reuse_det_result=reuse_det_result)
522
+
523
+ # mot output format: id, class, score, xmin, ymin, xmax, ymax
524
+ mot_res = parse_mot_res(res)
525
+ if frame_id > self.warmup_frame:
526
+ self.pipe_timer.module_time['mot'].end()
527
+ self.pipe_timer.track_num += len(mot_res['boxes'])
528
+
529
+ if frame_id % 10 == 0:
530
+ print("Thread: {}; trackid number: {}".format(
531
+ thread_idx, len(mot_res['boxes'])))
532
+
533
+ # flow_statistic only support single class MOT
534
+ boxes, scores, ids = res[0] # batch size = 1 in MOT
535
+ mot_result = (frame_id + 1, boxes[0], scores[0],
536
+ ids[0]) # single class
537
+ statistic = flow_statistic(
538
+ mot_result,
539
+ self.secs_interval,
540
+ self.do_entrance_counting,
541
+ self.do_break_in_counting,
542
+ self.region_type,
543
+ video_fps,
544
+ entrance,
545
+ id_set,
546
+ interval_id_set,
547
+ in_id_list,
548
+ out_id_list,
549
+ prev_center,
550
+ records,
551
+ ids2names=self.mot_predictor.pred_config.labels)
552
+ records = statistic['records']
553
+
554
+ if self.illegal_parking_time != -1:
555
+ object_in_region_info, illegal_parking_dict = update_object_info(
556
+ object_in_region_info, mot_result, self.region_type,
557
+ entrance, video_fps, self.illegal_parking_time)
558
+ if len(illegal_parking_dict) != 0:
559
+ # build relationship between id and plate
560
+ for key, value in illegal_parking_dict.items():
561
+ plate = self.collector.get_carlp(key)
562
+ illegal_parking_dict[key]['plate'] = plate
563
+
564
+ # nothing detected
565
+ if len(mot_res['boxes']) == 0:
566
+ frame_id += 1
567
+ if frame_id > self.warmup_frame:
568
+ self.pipe_timer.img_num += 1
569
+ self.pipe_timer.total_time.end()
570
+ if self.cfg['visual']:
571
+ _, _, fps = self.pipe_timer.get_total_time()
572
+ im = self.visualize_video(frame, mot_res, frame_id,
573
+ fps, entrance, records,
574
+ center_traj) # visualize
575
+ if len(self.pushurl) > 0:
576
+ pushstream.pipe.stdin.write(im.tobytes())
577
+ else:
578
+ writer.write(im)
579
+ if self.file_name is None: # use camera_id
580
+ cv2.imshow('Paddle-Pipeline', im)
581
+ if cv2.waitKey(1) & 0xFF == ord('q'):
582
+ break
583
+ continue
584
+
585
+ self.pipeline_res.update(mot_res, 'mot')
586
+ crop_input, new_bboxes, ori_bboxes = crop_image_with_mot(
587
+ frame_rgb, mot_res)
588
+
589
+ if self.with_human_attr:
590
+ if frame_id > self.warmup_frame:
591
+ self.pipe_timer.module_time['attr'].start()
592
+ attr_res = self.attr_predictor.predict_image(
593
+ crop_input, visual=False)
594
+ if frame_id > self.warmup_frame:
595
+ self.pipe_timer.module_time['attr'].end()
596
+ self.pipeline_res.update(attr_res, 'attr')
597
+
598
+ if self.with_idbased_detaction:
599
+ if frame_id > self.warmup_frame:
600
+ self.pipe_timer.module_time['det_action'].start()
601
+ det_action_res = self.det_action_predictor.predict(
602
+ crop_input, mot_res)
603
+ if frame_id > self.warmup_frame:
604
+ self.pipe_timer.module_time['det_action'].end()
605
+ self.pipeline_res.update(det_action_res, 'det_action')
606
+
607
+ if self.cfg['visual']:
608
+ self.det_action_visual_helper.update(det_action_res)
609
+
610
+ if self.with_idbased_clsaction:
611
+ if frame_id > self.warmup_frame:
612
+ self.pipe_timer.module_time['cls_action'].start()
613
+ cls_action_res = self.cls_action_predictor.predict_with_mot(
614
+ crop_input, mot_res)
615
+ if frame_id > self.warmup_frame:
616
+ self.pipe_timer.module_time['cls_action'].end()
617
+ self.pipeline_res.update(cls_action_res, 'cls_action')
618
+
619
+ if self.cfg['visual']:
620
+ self.cls_action_visual_helper.update(cls_action_res)
621
+
622
+ if self.with_skeleton_action:
623
+ if frame_id > self.warmup_frame:
624
+ self.pipe_timer.module_time['kpt'].start()
625
+ kpt_pred = self.kpt_predictor.predict_image(
626
+ crop_input, visual=False)
627
+ keypoint_vector, score_vector = translate_to_ori_images(
628
+ kpt_pred, np.array(new_bboxes))
629
+ kpt_res = {}
630
+ kpt_res['keypoint'] = [
631
+ keypoint_vector.tolist(), score_vector.tolist()
632
+ ] if len(keypoint_vector) > 0 else [[], []]
633
+ kpt_res['bbox'] = ori_bboxes
634
+ if frame_id > self.warmup_frame:
635
+ self.pipe_timer.module_time['kpt'].end()
636
+
637
+ self.pipeline_res.update(kpt_res, 'kpt')
638
+
639
+ self.kpt_buff.update(kpt_res,
640
+ mot_res) # collect kpt output
641
+ state = self.kpt_buff.get_state(
642
+ ) # whether frame num is enough or lost tracker
643
+
644
+ skeleton_action_res = {}
645
+ if state:
646
+ if frame_id > self.warmup_frame:
647
+ self.pipe_timer.module_time[
648
+ 'skeleton_action'].start()
649
+ collected_keypoint = self.kpt_buff.get_collected_keypoint(
650
+ ) # reoragnize kpt output with ID
651
+ skeleton_action_input = parse_mot_keypoint(
652
+ collected_keypoint, self.coord_size)
653
+ skeleton_action_res = self.skeleton_action_predictor.predict_skeleton_with_mot(
654
+ skeleton_action_input)
655
+ if frame_id > self.warmup_frame:
656
+ self.pipe_timer.module_time['skeleton_action'].end(
657
+ )
658
+ self.pipeline_res.update(skeleton_action_res,
659
+ 'skeleton_action')
660
+
661
+ if self.cfg['visual']:
662
+ self.skeleton_action_visual_helper.update(
663
+ skeleton_action_res)
664
+
665
+ if self.with_mtmct and frame_id % 10 == 0:
666
+ crop_input, img_qualities, rects = self.reid_predictor.crop_image_with_mot(
667
+ frame_rgb, mot_res)
668
+ if frame_id > self.warmup_frame:
669
+ self.pipe_timer.module_time['reid'].start()
670
+ reid_res = self.reid_predictor.predict_batch(crop_input)
671
+
672
+ if frame_id > self.warmup_frame:
673
+ self.pipe_timer.module_time['reid'].end()
674
+
675
+ reid_res_dict = {
676
+ 'features': reid_res,
677
+ "qualities": img_qualities,
678
+ "rects": rects
679
+ }
680
+ self.pipeline_res.update(reid_res_dict, 'reid')
681
+ else:
682
+ self.pipeline_res.clear('reid')
683
+
684
+ if self.with_video_action:
685
+ # get the params
686
+ frame_len = self.cfg["VIDEO_ACTION"]["frame_len"]
687
+ sample_freq = self.cfg["VIDEO_ACTION"]["sample_freq"]
688
+
689
+ if sample_freq * frame_len > frame_count: # video is too short
690
+ sample_freq = int(frame_count / frame_len)
691
+
692
+ # filter the warmup frames
693
+ if frame_id > self.warmup_frame:
694
+ self.pipe_timer.module_time['video_action'].start()
695
+
696
+ # collect frames
697
+ if frame_id % sample_freq == 0:
698
+ # Scale image
699
+ scaled_img = scale(frame_rgb)
700
+ video_action_imgs.append(scaled_img)
701
+
702
+ # the number of collected frames is enough to predict video action
703
+ if len(video_action_imgs) == frame_len:
704
+ classes, scores = self.video_action_predictor.predict(
705
+ video_action_imgs)
706
+ if frame_id > self.warmup_frame:
707
+ self.pipe_timer.module_time['video_action'].end()
708
+
709
+ video_action_res = {
710
+ "class": classes[0],
711
+ "score": scores[0]
712
+ }
713
+ self.pipeline_res.update(video_action_res, 'video_action')
714
+
715
+ print("video_action_res:", video_action_res)
716
+
717
+ video_action_imgs.clear() # next clip
718
+
719
+ self.collector.append(frame_id, self.pipeline_res)
720
+
721
+ if frame_id > self.warmup_frame:
722
+ self.pipe_timer.img_num += 1
723
+ self.pipe_timer.total_time.end()
724
+ frame_id += 1
725
+
726
+ if self.cfg['visual']:
727
+ _, _, fps = self.pipe_timer.get_total_time()
728
+
729
+ im = self.visualize_video(frame, self.pipeline_res,
730
+ self.collector, frame_id, fps,
731
+ entrance, records, center_traj,
732
+ self.illegal_parking_time != -1,
733
+ illegal_parking_dict) # visualize
734
+ if len(self.pushurl) > 0:
735
+ pushstream.pipe.stdin.write(im.tobytes())
736
+ else:
737
+ writer.write(im)
738
+ if self.cfg['visual'] and len(self.pushurl) == 0:
739
+ writer.release()
740
+
741
+ return out_path
742
+
743
+ def visualize_video(self,
744
+ image,
745
+ result,
746
+ collector,
747
+ frame_id,
748
+ fps,
749
+ entrance=None,
750
+ records=None,
751
+ center_traj=None,
752
+ do_illegal_parking_recognition=False,
753
+ illegal_parking_dict=None):
754
+ mot_res = copy.deepcopy(result.get('mot'))
755
+ if mot_res is not None:
756
+ ids = mot_res['boxes'][:, 0]
757
+ scores = mot_res['boxes'][:, 2]
758
+ boxes = mot_res['boxes'][:, 3:]
759
+ boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
760
+ boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
761
+ else:
762
+ boxes = np.zeros([0, 4])
763
+ ids = np.zeros([0])
764
+ scores = np.zeros([0])
765
+
766
+ # single class, still need to be defaultdict type for ploting
767
+ num_classes = 1
768
+ online_tlwhs = defaultdict(list)
769
+ online_scores = defaultdict(list)
770
+ online_ids = defaultdict(list)
771
+ online_tlwhs[0] = boxes
772
+ online_scores[0] = scores
773
+ online_ids[0] = ids
774
+
775
+ if mot_res is not None:
776
+ image = plot_tracking_dict(
777
+ image,
778
+ num_classes,
779
+ online_tlwhs,
780
+ online_ids,
781
+ online_scores,
782
+ frame_id=frame_id,
783
+ fps=fps,
784
+ ids2names=self.mot_predictor.pred_config.labels,
785
+ do_entrance_counting=self.do_entrance_counting,
786
+ do_break_in_counting=self.do_break_in_counting,
787
+ do_illegal_parking_recognition=do_illegal_parking_recognition,
788
+ illegal_parking_dict=illegal_parking_dict,
789
+ entrance=entrance,
790
+ records=records,
791
+ center_traj=center_traj)
792
+
793
+ human_attr_res = result.get('attr')
794
+ if human_attr_res is not None:
795
+ boxes = mot_res['boxes'][:, 1:]
796
+ human_attr_res = human_attr_res['output']
797
+ image = visualize_attr(image, human_attr_res, boxes)
798
+ image = np.array(image)
799
+
800
+ kpt_res = result.get('kpt')
801
+ if kpt_res is not None:
802
+ image = visualize_pose(
803
+ image,
804
+ kpt_res,
805
+ visual_thresh=self.cfg['kpt_thresh'],
806
+ returnimg=True)
807
+
808
+ video_action_res = result.get('video_action')
809
+ if video_action_res is not None:
810
+ video_action_score = None
811
+ if video_action_res and video_action_res["class"] == 1:
812
+ video_action_score = video_action_res["score"]
813
+ mot_boxes = None
814
+ if mot_res:
815
+ mot_boxes = mot_res['boxes']
816
+ image = visualize_action(
817
+ image,
818
+ mot_boxes,
819
+ action_visual_collector=None,
820
+ action_text="SkeletonAction",
821
+ video_action_score=video_action_score,
822
+ video_action_text="Fight")
823
+
824
+ visual_helper_for_display = []
825
+ action_to_display = []
826
+
827
+ skeleton_action_res = result.get('skeleton_action')
828
+ if skeleton_action_res is not None:
829
+ visual_helper_for_display.append(
830
+ self.skeleton_action_visual_helper)
831
+ action_to_display.append("Falling")
832
+
833
+ det_action_res = result.get('det_action')
834
+ if det_action_res is not None:
835
+ visual_helper_for_display.append(self.det_action_visual_helper)
836
+ action_to_display.append("Smoking")
837
+
838
+ cls_action_res = result.get('cls_action')
839
+ if cls_action_res is not None:
840
+ visual_helper_for_display.append(self.cls_action_visual_helper)
841
+ action_to_display.append("Calling")
842
+
843
+ if len(visual_helper_for_display) > 0:
844
+ image = visualize_action(image, mot_res['boxes'],
845
+ visual_helper_for_display,
846
+ action_to_display)
847
+
848
+ return image
849
+
850
+ def visualize_image(self, images, result):
851
+
852
+ det_res = result.get('det')
853
+ human_attr_res = result.get('attr')
854
+ vehicle_attr_res = result.get('vehicle_attr')
855
+ vehicleplate_res = result.get('vehicleplate')
856
+
857
+ if det_res is not None:
858
+ det_res_i = {}
859
+ boxes_num_i = det_res['boxes_num'][0]
860
+ det_res_i['boxes'] = det_res['boxes'][0:0 + boxes_num_i, :]
861
+ im = visualize_box_mask(
862
+ images[0],
863
+ det_res_i,
864
+ labels=['target'],
865
+ threshold=self.cfg['crop_thresh'])
866
+ im = np.ascontiguousarray(np.copy(im))
867
+ im = cv2.cvtColor(im, cv2.COLOR_RGB2BGR)
868
+ if human_attr_res is not None:
869
+ human_attr_res_i = human_attr_res['output'][0:0 + boxes_num_i]
870
+ im = visualize_attr(im, human_attr_res_i, det_res_i['boxes'])
871
+
872
+ return im
873
+
874
+
875
+ def pp_humanv2(input_date, avtivity_list):
876
+
877
+ paddle.enable_static()
878
+
879
+ # parse params from command
880
+ parser = argsparser()
881
+ FLAGS = parser.parse_args()
882
+ FLAGS.device = FLAGS.device.upper()
883
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
884
+ ], "device should be CPU, GPU or XPU"
885
+
886
+ cfg = merge_cfg(FLAGS) # use command params to update config
887
+
888
+ if isinstance(input_date, str):
889
+ FLAGS.video_file = input_date
890
+ else:
891
+ FLAGS.image_file = input_date
892
+
893
+ FLAGS.avtivity_list = avtivity_list
894
+
895
+ pipeline = Pipeline(FLAGS, cfg)
896
+ out = pipeline.run_multithreads()
897
+
898
+ return out
pipeline/pphuman/action_infer.py ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+
19
+ import cv2
20
+ import numpy as np
21
+ import math
22
+ import paddle
23
+ import sys
24
+
25
+ # add deploy path of PadleDetection to sys.path
26
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2)))
27
+ sys.path.insert(0, parent_path)
28
+
29
+ from paddle.inference import Config, create_predictor
30
+ from python.utils import argsparser, Timer, get_current_memory_mb
31
+ from python.benchmark_utils import PaddleInferBenchmark
32
+ from python.infer import Detector, print_arguments
33
+ from attr_infer import AttrDetector
34
+
35
+
36
+ class SkeletonActionRecognizer(Detector):
37
+ """
38
+ Args:
39
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
40
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
41
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
42
+ batch_size (int): size of pre batch in inference
43
+ trt_min_shape (int): min shape for dynamic shape in trt
44
+ trt_max_shape (int): max shape for dynamic shape in trt
45
+ trt_opt_shape (int): opt shape for dynamic shape in trt
46
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
47
+ calibration, trt_calib_mode need to set True
48
+ cpu_threads (int): cpu threads
49
+ enable_mkldnn (bool): whether to open MKLDNN
50
+ threshold (float): The threshold of score for visualization
51
+ window_size(int): Temporal size of skeleton feature.
52
+ random_pad (bool): Whether do random padding when frame length < window_size.
53
+ """
54
+
55
+ def __init__(self,
56
+ model_dir,
57
+ device='CPU',
58
+ run_mode='paddle',
59
+ batch_size=1,
60
+ trt_min_shape=1,
61
+ trt_max_shape=1280,
62
+ trt_opt_shape=640,
63
+ trt_calib_mode=False,
64
+ cpu_threads=1,
65
+ enable_mkldnn=False,
66
+ output_dir='output',
67
+ threshold=0.5,
68
+ window_size=100,
69
+ random_pad=False):
70
+ assert batch_size == 1, "SkeletonActionRecognizer only support batch_size=1 now."
71
+ super(SkeletonActionRecognizer, self).__init__(
72
+ model_dir=model_dir,
73
+ device=device,
74
+ run_mode=run_mode,
75
+ batch_size=batch_size,
76
+ trt_min_shape=trt_min_shape,
77
+ trt_max_shape=trt_max_shape,
78
+ trt_opt_shape=trt_opt_shape,
79
+ trt_calib_mode=trt_calib_mode,
80
+ cpu_threads=cpu_threads,
81
+ enable_mkldnn=enable_mkldnn,
82
+ output_dir=output_dir,
83
+ threshold=threshold,
84
+ delete_shuffle_pass=True)
85
+
86
+ @classmethod
87
+ def init_with_cfg(cls, args, cfg):
88
+ return cls(model_dir=cfg['model_dir'],
89
+ batch_size=cfg['batch_size'],
90
+ window_size=cfg['max_frames'],
91
+ device=args.device,
92
+ run_mode=args.run_mode,
93
+ trt_min_shape=args.trt_min_shape,
94
+ trt_max_shape=args.trt_max_shape,
95
+ trt_opt_shape=args.trt_opt_shape,
96
+ trt_calib_mode=args.trt_calib_mode,
97
+ cpu_threads=args.cpu_threads,
98
+ enable_mkldnn=args.enable_mkldnn)
99
+
100
+ def predict(self, repeats=1):
101
+ '''
102
+ Args:
103
+ repeats (int): repeat number for prediction
104
+ Returns:
105
+ results (dict):
106
+ '''
107
+ # model prediction
108
+ output_names = self.predictor.get_output_names()
109
+ for i in range(repeats):
110
+ self.predictor.run()
111
+ output_tensor = self.predictor.get_output_handle(output_names[0])
112
+ np_output = output_tensor.copy_to_cpu()
113
+ result = dict(output=np_output)
114
+ return result
115
+
116
+ def predict_skeleton(self, skeleton_list, run_benchmark=False, repeats=1):
117
+ results = []
118
+ for i, skeleton in enumerate(skeleton_list):
119
+ if run_benchmark:
120
+ # preprocess
121
+ inputs = self.preprocess(skeleton) # warmup
122
+ self.det_times.preprocess_time_s.start()
123
+ inputs = self.preprocess(skeleton)
124
+ self.det_times.preprocess_time_s.end()
125
+
126
+ # model prediction
127
+ result = self.predict(repeats=repeats) # warmup
128
+ self.det_times.inference_time_s.start()
129
+ result = self.predict(repeats=repeats)
130
+ self.det_times.inference_time_s.end(repeats=repeats)
131
+
132
+ # postprocess
133
+ result_warmup = self.postprocess(inputs, result) # warmup
134
+ self.det_times.postprocess_time_s.start()
135
+ result = self.postprocess(inputs, result)
136
+ self.det_times.postprocess_time_s.end()
137
+ self.det_times.img_num += len(skeleton)
138
+
139
+ cm, gm, gu = get_current_memory_mb()
140
+ self.cpu_mem += cm
141
+ self.gpu_mem += gm
142
+ self.gpu_util += gu
143
+ else:
144
+ # preprocess
145
+ self.det_times.preprocess_time_s.start()
146
+ inputs = self.preprocess(skeleton)
147
+ self.det_times.preprocess_time_s.end()
148
+
149
+ # model prediction
150
+ self.det_times.inference_time_s.start()
151
+ result = self.predict()
152
+ self.det_times.inference_time_s.end()
153
+
154
+ # postprocess
155
+ self.det_times.postprocess_time_s.start()
156
+ result = self.postprocess(inputs, result)
157
+ self.det_times.postprocess_time_s.end()
158
+ self.det_times.img_num += len(skeleton)
159
+
160
+ results.append(result)
161
+ return results
162
+
163
+ def predict_skeleton_with_mot(self, skeleton_with_mot,
164
+ run_benchmark=False):
165
+ """
166
+ skeleton_with_mot (dict): includes individual skeleton sequences, which shape is [C, T, K, 1]
167
+ and its corresponding track id.
168
+ """
169
+
170
+ skeleton_list = skeleton_with_mot["skeleton"]
171
+ mot_id = skeleton_with_mot["mot_id"]
172
+ act_res = self.predict_skeleton(
173
+ skeleton_list, run_benchmark, repeats=1)
174
+ results = list(zip(mot_id, act_res))
175
+ return results
176
+
177
+ def preprocess(self, data):
178
+ preprocess_ops = []
179
+ for op_info in self.pred_config.preprocess_infos:
180
+ new_op_info = op_info.copy()
181
+ op_type = new_op_info.pop('type')
182
+ preprocess_ops.append(eval(op_type)(**new_op_info))
183
+
184
+ input_lst = []
185
+ data = action_preprocess(data, preprocess_ops)
186
+ input_lst.append(data)
187
+ input_names = self.predictor.get_input_names()
188
+ inputs = {}
189
+ inputs['data_batch_0'] = np.stack(input_lst, axis=0).astype('float32')
190
+
191
+ for i in range(len(input_names)):
192
+ input_tensor = self.predictor.get_input_handle(input_names[i])
193
+ input_tensor.copy_from_cpu(inputs[input_names[i]])
194
+
195
+ return inputs
196
+
197
+ def postprocess(self, inputs, result):
198
+ # postprocess output of predictor
199
+ output_logit = result['output'][0]
200
+ classes = np.argpartition(output_logit, -1)[-1:]
201
+ classes = classes[np.argsort(-output_logit[classes])]
202
+ scores = output_logit[classes]
203
+ result = {'class': classes, 'score': scores}
204
+ return result
205
+
206
+
207
+ def action_preprocess(input, preprocess_ops):
208
+ """
209
+ input (str | numpy.array): if input is str, it should be a legal file path with numpy array saved.
210
+ Otherwise it should be numpy.array as direct input.
211
+ return (numpy.array)
212
+ """
213
+ if isinstance(input, str):
214
+ assert os.path.isfile(input) is not None, "{0} not exists".format(
215
+ input)
216
+ data = np.load(input)
217
+ else:
218
+ data = input
219
+ for operator in preprocess_ops:
220
+ data = operator(data)
221
+ return data
222
+
223
+
224
+ class AutoPadding(object):
225
+ """
226
+ Sample or Padding frame skeleton feature.
227
+ Args:
228
+ window_size (int): Temporal size of skeleton feature.
229
+ random_pad (bool): Whether do random padding when frame length < window size. Default: False.
230
+ """
231
+
232
+ def __init__(self, window_size=100, random_pad=False):
233
+ self.window_size = window_size
234
+ self.random_pad = random_pad
235
+
236
+ def get_frame_num(self, data):
237
+ C, T, V, M = data.shape
238
+ for i in range(T - 1, -1, -1):
239
+ tmp = np.sum(data[:, i, :, :])
240
+ if tmp > 0:
241
+ T = i + 1
242
+ break
243
+ return T
244
+
245
+ def __call__(self, results):
246
+ data = results
247
+
248
+ C, T, V, M = data.shape
249
+ T = self.get_frame_num(data)
250
+ if T == self.window_size:
251
+ data_pad = data[:, :self.window_size, :, :]
252
+ elif T < self.window_size:
253
+ begin = random.randint(
254
+ 0, self.window_size - T) if self.random_pad else 0
255
+ data_pad = np.zeros((C, self.window_size, V, M))
256
+ data_pad[:, begin:begin + T, :, :] = data[:, :T, :, :]
257
+ else:
258
+ if self.random_pad:
259
+ index = np.random.choice(
260
+ T, self.window_size, replace=False).astype('int64')
261
+ else:
262
+ index = np.linspace(0, T, self.window_size).astype("int64")
263
+ data_pad = data[:, index, :, :]
264
+
265
+ return data_pad
266
+
267
+
268
+ def get_test_skeletons(input_file):
269
+ assert input_file is not None, "--action_file can not be None"
270
+ input_data = np.load(input_file)
271
+ if input_data.ndim == 4:
272
+ return [input_data]
273
+ elif input_data.ndim == 5:
274
+ output = list(
275
+ map(lambda x: np.squeeze(x, 0),
276
+ np.split(input_data, input_data.shape[0], 0)))
277
+ return output
278
+ else:
279
+ raise ValueError(
280
+ "Now only support input with shape: (N, C, T, K, M) or (C, T, K, M)"
281
+ )
282
+
283
+
284
+ class DetActionRecognizer(object):
285
+ """
286
+ Args:
287
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
288
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
289
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
290
+ batch_size (int): size of pre batch in inference
291
+ trt_min_shape (int): min shape for dynamic shape in trt
292
+ trt_max_shape (int): max shape for dynamic shape in trt
293
+ trt_opt_shape (int): opt shape for dynamic shape in trt
294
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
295
+ calibration, trt_calib_mode need to set True
296
+ cpu_threads (int): cpu threads
297
+ enable_mkldnn (bool): whether to open MKLDNN
298
+ threshold (float): The threshold of score for action feature object detection.
299
+ display_frames (int): The duration for corresponding detected action.
300
+ skip_frame_num (int): The number of frames for interval prediction. A skipped frame will
301
+ reuse the result of its last frame. If it is set to 0, no frame will be skipped. Default
302
+ is 0.
303
+
304
+ """
305
+
306
+ def __init__(self,
307
+ model_dir,
308
+ device='CPU',
309
+ run_mode='paddle',
310
+ batch_size=1,
311
+ trt_min_shape=1,
312
+ trt_max_shape=1280,
313
+ trt_opt_shape=640,
314
+ trt_calib_mode=False,
315
+ cpu_threads=1,
316
+ enable_mkldnn=False,
317
+ output_dir='output',
318
+ threshold=0.5,
319
+ display_frames=20,
320
+ skip_frame_num=0):
321
+ super(DetActionRecognizer, self).__init__()
322
+ self.detector = Detector(
323
+ model_dir=model_dir,
324
+ device=device,
325
+ run_mode=run_mode,
326
+ batch_size=batch_size,
327
+ trt_min_shape=trt_min_shape,
328
+ trt_max_shape=trt_max_shape,
329
+ trt_opt_shape=trt_opt_shape,
330
+ trt_calib_mode=trt_calib_mode,
331
+ cpu_threads=cpu_threads,
332
+ enable_mkldnn=enable_mkldnn,
333
+ output_dir=output_dir,
334
+ threshold=threshold)
335
+ self.threshold = threshold
336
+ self.frame_life = display_frames
337
+ self.result_history = {}
338
+ self.skip_frame_num = skip_frame_num
339
+ self.skip_frame_cnt = 0
340
+ self.id_in_last_frame = []
341
+
342
+ @classmethod
343
+ def init_with_cfg(cls, args, cfg):
344
+ return cls(model_dir=cfg['model_dir'],
345
+ batch_size=cfg['batch_size'],
346
+ threshold=cfg['threshold'],
347
+ display_frames=cfg['display_frames'],
348
+ skip_frame_num=cfg['skip_frame_num'],
349
+ device=args.device,
350
+ run_mode=args.run_mode,
351
+ trt_min_shape=args.trt_min_shape,
352
+ trt_max_shape=args.trt_max_shape,
353
+ trt_opt_shape=args.trt_opt_shape,
354
+ trt_calib_mode=args.trt_calib_mode,
355
+ cpu_threads=args.cpu_threads,
356
+ enable_mkldnn=args.enable_mkldnn)
357
+
358
+ def predict(self, images, mot_result):
359
+ if self.skip_frame_cnt == 0 or (not self.check_id_is_same(mot_result)):
360
+ det_result = self.detector.predict_image(images, visual=False)
361
+ result = self.postprocess(det_result, mot_result)
362
+ else:
363
+ result = self.reuse_result(mot_result)
364
+
365
+ self.skip_frame_cnt += 1
366
+ if self.skip_frame_cnt >= self.skip_frame_num:
367
+ self.skip_frame_cnt = 0
368
+
369
+ return result
370
+
371
+ def postprocess(self, det_result, mot_result):
372
+ np_boxes_num = det_result['boxes_num']
373
+ if np_boxes_num[0] <= 0:
374
+ return [[], []]
375
+
376
+ mot_bboxes = mot_result.get('boxes')
377
+
378
+ cur_box_idx = 0
379
+ mot_id = []
380
+ act_res = []
381
+ for idx in range(len(mot_bboxes)):
382
+ tracker_id = mot_bboxes[idx, 0]
383
+
384
+ # Current now, class 0 is positive, class 1 is negative.
385
+ action_ret = {'class': 1.0, 'score': -1.0}
386
+ box_num = np_boxes_num[idx]
387
+ boxes = det_result['boxes'][cur_box_idx:cur_box_idx + box_num]
388
+ cur_box_idx += box_num
389
+ isvalid = (boxes[:, 1] > self.threshold) & (boxes[:, 0] == 0)
390
+ valid_boxes = boxes[isvalid, :]
391
+
392
+ if valid_boxes.shape[0] >= 1:
393
+ action_ret['class'] = valid_boxes[0, 0]
394
+ action_ret['score'] = valid_boxes[0, 1]
395
+ self.result_history[
396
+ tracker_id] = [0, self.frame_life, valid_boxes[0, 1]]
397
+ else:
398
+ history_det, life_remain, history_score = self.result_history.get(
399
+ tracker_id, [1, self.frame_life, -1.0])
400
+ action_ret['class'] = history_det
401
+ action_ret['score'] = -1.0
402
+ life_remain -= 1
403
+ if life_remain <= 0 and tracker_id in self.result_history:
404
+ del (self.result_history[tracker_id])
405
+ elif tracker_id in self.result_history:
406
+ self.result_history[tracker_id][1] = life_remain
407
+ else:
408
+ self.result_history[tracker_id] = [
409
+ history_det, life_remain, history_score
410
+ ]
411
+
412
+ mot_id.append(tracker_id)
413
+ act_res.append(action_ret)
414
+ result = list(zip(mot_id, act_res))
415
+ self.id_in_last_frame = mot_id
416
+
417
+ return result
418
+
419
+ def check_id_is_same(self, mot_result):
420
+ mot_bboxes = mot_result.get('boxes')
421
+ for idx in range(len(mot_bboxes)):
422
+ tracker_id = mot_bboxes[idx, 0]
423
+ if tracker_id not in self.id_in_last_frame:
424
+ return False
425
+ return True
426
+
427
+ def reuse_result(self, mot_result):
428
+ # This function reusing previous results of the same ID directly.
429
+ mot_bboxes = mot_result.get('boxes')
430
+
431
+ mot_id = []
432
+ act_res = []
433
+
434
+ for idx in range(len(mot_bboxes)):
435
+ tracker_id = mot_bboxes[idx, 0]
436
+ history_cls, life_remain, history_score = self.result_history.get(
437
+ tracker_id, [1, 0, -1.0])
438
+
439
+ life_remain -= 1
440
+ if tracker_id in self.result_history:
441
+ self.result_history[tracker_id][1] = life_remain
442
+
443
+ action_ret = {'class': history_cls, 'score': history_score}
444
+ mot_id.append(tracker_id)
445
+ act_res.append(action_ret)
446
+
447
+ result = list(zip(mot_id, act_res))
448
+ self.id_in_last_frame = mot_id
449
+
450
+ return result
451
+
452
+
453
+ class ClsActionRecognizer(AttrDetector):
454
+ """
455
+ Args:
456
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
457
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
458
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
459
+ batch_size (int): size of pre batch in inference
460
+ trt_min_shape (int): min shape for dynamic shape in trt
461
+ trt_max_shape (int): max shape for dynamic shape in trt
462
+ trt_opt_shape (int): opt shape for dynamic shape in trt
463
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
464
+ calibration, trt_calib_mode need to set True
465
+ cpu_threads (int): cpu threads
466
+ enable_mkldnn (bool): whether to open MKLDNN
467
+ threshold (float): The threshold of score for action feature object detection.
468
+ display_frames (int): The duration for corresponding detected action.
469
+ skip_frame_num (int): The number of frames for interval prediction. A skipped frame will
470
+ reuse the result of its last frame. If it is set to 0, no frame will be skipped. Default
471
+ is 0.
472
+ """
473
+
474
+ def __init__(self,
475
+ model_dir,
476
+ device='CPU',
477
+ run_mode='paddle',
478
+ batch_size=1,
479
+ trt_min_shape=1,
480
+ trt_max_shape=1280,
481
+ trt_opt_shape=640,
482
+ trt_calib_mode=False,
483
+ cpu_threads=1,
484
+ enable_mkldnn=False,
485
+ output_dir='output',
486
+ threshold=0.5,
487
+ display_frames=80,
488
+ skip_frame_num=0):
489
+ super(ClsActionRecognizer, self).__init__(
490
+ model_dir=model_dir,
491
+ device=device,
492
+ run_mode=run_mode,
493
+ batch_size=batch_size,
494
+ trt_min_shape=trt_min_shape,
495
+ trt_max_shape=trt_max_shape,
496
+ trt_opt_shape=trt_opt_shape,
497
+ trt_calib_mode=trt_calib_mode,
498
+ cpu_threads=cpu_threads,
499
+ enable_mkldnn=enable_mkldnn,
500
+ output_dir=output_dir,
501
+ threshold=threshold)
502
+ self.threshold = threshold
503
+ self.frame_life = display_frames
504
+ self.result_history = {}
505
+ self.skip_frame_num = skip_frame_num
506
+ self.skip_frame_cnt = 0
507
+ self.id_in_last_frame = []
508
+
509
+ @classmethod
510
+ def init_with_cfg(cls, args, cfg):
511
+ return cls(model_dir=cfg['model_dir'],
512
+ batch_size=cfg['batch_size'],
513
+ threshold=cfg['threshold'],
514
+ display_frames=cfg['display_frames'],
515
+ skip_frame_num=cfg['skip_frame_num'],
516
+ device=args.device,
517
+ run_mode=args.run_mode,
518
+ trt_min_shape=args.trt_min_shape,
519
+ trt_max_shape=args.trt_max_shape,
520
+ trt_opt_shape=args.trt_opt_shape,
521
+ trt_calib_mode=args.trt_calib_mode,
522
+ cpu_threads=args.cpu_threads,
523
+ enable_mkldnn=args.enable_mkldnn)
524
+
525
+ def predict_with_mot(self, images, mot_result):
526
+ if self.skip_frame_cnt == 0 or (not self.check_id_is_same(mot_result)):
527
+ images = self.crop_half_body(images)
528
+ cls_result = self.predict_image(images, visual=False)["output"]
529
+ result = self.match_action_with_id(cls_result, mot_result)
530
+ else:
531
+ result = self.reuse_result(mot_result)
532
+
533
+ self.skip_frame_cnt += 1
534
+ if self.skip_frame_cnt >= self.skip_frame_num:
535
+ self.skip_frame_cnt = 0
536
+
537
+ return result
538
+
539
+ def crop_half_body(self, images):
540
+ crop_images = []
541
+ for image in images:
542
+ h = image.shape[0]
543
+ crop_images.append(image[:h // 2 + 1, :, :])
544
+ return crop_images
545
+
546
+ def postprocess(self, inputs, result):
547
+ # postprocess output of predictor
548
+ im_results = result['output']
549
+ batch_res = []
550
+ for res in im_results:
551
+ action_res = res.tolist()
552
+ for cid, score in enumerate(action_res):
553
+ action_res[cid] = score
554
+ batch_res.append(action_res)
555
+ result = {'output': batch_res}
556
+ return result
557
+
558
+ def match_action_with_id(self, cls_result, mot_result):
559
+ mot_bboxes = mot_result.get('boxes')
560
+
561
+ mot_id = []
562
+ act_res = []
563
+
564
+ for idx in range(len(mot_bboxes)):
565
+ tracker_id = mot_bboxes[idx, 0]
566
+
567
+ cls_id_res = 1
568
+ cls_score_res = -1.0
569
+ for cls_id in range(len(cls_result[idx])):
570
+ score = cls_result[idx][cls_id]
571
+ if score > cls_score_res:
572
+ cls_id_res = cls_id
573
+ cls_score_res = score
574
+
575
+ # Current now, class 0 is positive, class 1 is negative.
576
+ if cls_id_res == 1 or (cls_id_res == 0 and
577
+ cls_score_res < self.threshold):
578
+ history_cls, life_remain, history_score = self.result_history.get(
579
+ tracker_id, [1, self.frame_life, -1.0])
580
+ cls_id_res = history_cls
581
+ cls_score_res = 1 - cls_score_res
582
+ life_remain -= 1
583
+ if life_remain <= 0 and tracker_id in self.result_history:
584
+ del (self.result_history[tracker_id])
585
+ elif tracker_id in self.result_history:
586
+ self.result_history[tracker_id][1] = life_remain
587
+ else:
588
+ self.result_history[tracker_id] = [
589
+ cls_id_res, life_remain, cls_score_res
590
+ ]
591
+ else:
592
+ self.result_history[tracker_id] = [
593
+ cls_id_res, self.frame_life, cls_score_res
594
+ ]
595
+
596
+ action_ret = {'class': cls_id_res, 'score': cls_score_res}
597
+ mot_id.append(tracker_id)
598
+ act_res.append(action_ret)
599
+ result = list(zip(mot_id, act_res))
600
+ self.id_in_last_frame = mot_id
601
+
602
+ return result
603
+
604
+ def check_id_is_same(self, mot_result):
605
+ mot_bboxes = mot_result.get('boxes')
606
+ for idx in range(len(mot_bboxes)):
607
+ tracker_id = mot_bboxes[idx, 0]
608
+ if tracker_id not in self.id_in_last_frame:
609
+ return False
610
+ return True
611
+
612
+ def reuse_result(self, mot_result):
613
+ # This function reusing previous results of the same ID directly.
614
+ mot_bboxes = mot_result.get('boxes')
615
+
616
+ mot_id = []
617
+ act_res = []
618
+
619
+ for idx in range(len(mot_bboxes)):
620
+ tracker_id = mot_bboxes[idx, 0]
621
+ history_cls, life_remain, history_score = self.result_history.get(
622
+ tracker_id, [1, 0, -1.0])
623
+
624
+ life_remain -= 1
625
+ if tracker_id in self.result_history:
626
+ self.result_history[tracker_id][1] = life_remain
627
+
628
+ action_ret = {'class': history_cls, 'score': history_score}
629
+ mot_id.append(tracker_id)
630
+ act_res.append(action_ret)
631
+
632
+ result = list(zip(mot_id, act_res))
633
+ self.id_in_last_frame = mot_id
634
+
635
+ return result
636
+
637
+
638
+ def main():
639
+ detector = SkeletonActionRecognizer(
640
+ FLAGS.model_dir,
641
+ device=FLAGS.device,
642
+ run_mode=FLAGS.run_mode,
643
+ batch_size=FLAGS.batch_size,
644
+ trt_min_shape=FLAGS.trt_min_shape,
645
+ trt_max_shape=FLAGS.trt_max_shape,
646
+ trt_opt_shape=FLAGS.trt_opt_shape,
647
+ trt_calib_mode=FLAGS.trt_calib_mode,
648
+ cpu_threads=FLAGS.cpu_threads,
649
+ enable_mkldnn=FLAGS.enable_mkldnn,
650
+ threshold=FLAGS.threshold,
651
+ output_dir=FLAGS.output_dir,
652
+ window_size=FLAGS.window_size,
653
+ random_pad=FLAGS.random_pad)
654
+ # predict from numpy array
655
+ input_list = get_test_skeletons(FLAGS.action_file)
656
+ detector.predict_skeleton(input_list, FLAGS.run_benchmark, repeats=10)
657
+ if not FLAGS.run_benchmark:
658
+ detector.det_times.info(average=True)
659
+ else:
660
+ mems = {
661
+ 'cpu_rss_mb': detector.cpu_mem / len(input_list),
662
+ 'gpu_rss_mb': detector.gpu_mem / len(input_list),
663
+ 'gpu_util': detector.gpu_util * 100 / len(input_list)
664
+ }
665
+
666
+ perf_info = detector.det_times.report(average=True)
667
+ model_dir = FLAGS.model_dir
668
+ mode = FLAGS.run_mode
669
+ model_info = {
670
+ 'model_name': model_dir.strip('/').split('/')[-1],
671
+ 'precision': mode.split('_')[-1]
672
+ }
673
+ data_info = {
674
+ 'batch_size': FLAGS.batch_size,
675
+ 'shape': "dynamic_shape",
676
+ 'data_num': perf_info['img_num']
677
+ }
678
+ det_log = PaddleInferBenchmark(detector.config, model_info, data_info,
679
+ perf_info, mems)
680
+ det_log('SkeletonAction')
681
+
682
+
683
+ if __name__ == '__main__':
684
+ paddle.enable_static()
685
+ parser = argsparser()
686
+ FLAGS = parser.parse_args()
687
+ print_arguments(FLAGS)
688
+ FLAGS.device = FLAGS.device.upper()
689
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
690
+ ], "device should be CPU, GPU or XPU"
691
+ assert not FLAGS.use_gpu, "use_gpu has been deprecated, please use --device"
692
+
693
+ main()
pipeline/pphuman/action_utils.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ class KeyPointSequence(object):
17
+ def __init__(self, max_size=100):
18
+ self.frames = 0
19
+ self.kpts = []
20
+ self.bboxes = []
21
+ self.max_size = max_size
22
+
23
+ def save(self, kpt, bbox):
24
+ self.kpts.append(kpt)
25
+ self.bboxes.append(bbox)
26
+ self.frames += 1
27
+ if self.frames == self.max_size:
28
+ return True
29
+ return False
30
+
31
+
32
+ class KeyPointBuff(object):
33
+ def __init__(self, max_size=100):
34
+ self.flag_track_interrupt = False
35
+ self.keypoint_saver = dict()
36
+ self.max_size = max_size
37
+ self.id_to_pop = set()
38
+ self.flag_to_pop = False
39
+
40
+ def get_state(self):
41
+ return self.flag_to_pop
42
+
43
+ def update(self, kpt_res, mot_res):
44
+ kpts = kpt_res.get('keypoint')[0]
45
+ bboxes = kpt_res.get('bbox')
46
+ mot_bboxes = mot_res.get('boxes')
47
+ updated_id = set()
48
+
49
+ for idx in range(len(kpts)):
50
+ tracker_id = mot_bboxes[idx, 0]
51
+ updated_id.add(tracker_id)
52
+
53
+ kpt_seq = self.keypoint_saver.get(tracker_id,
54
+ KeyPointSequence(self.max_size))
55
+ is_full = kpt_seq.save(kpts[idx], bboxes[idx])
56
+ self.keypoint_saver[tracker_id] = kpt_seq
57
+
58
+ #Scene1: result should be popped when frames meet max size
59
+ if is_full:
60
+ self.id_to_pop.add(tracker_id)
61
+ self.flag_to_pop = True
62
+
63
+ #Scene2: result of a lost tracker should be popped
64
+ interrupted_id = set(self.keypoint_saver.keys()) - updated_id
65
+ if len(interrupted_id) > 0:
66
+ self.flag_to_pop = True
67
+ self.id_to_pop.update(interrupted_id)
68
+
69
+ def get_collected_keypoint(self):
70
+ """
71
+ Output (List): List of keypoint results for Skeletonbased Recognition task, where
72
+ the format of each element is [tracker_id, KeyPointSequence of tracker_id]
73
+ """
74
+ output = []
75
+ for tracker_id in self.id_to_pop:
76
+ output.append([tracker_id, self.keypoint_saver[tracker_id]])
77
+ del (self.keypoint_saver[tracker_id])
78
+ self.flag_to_pop = False
79
+ self.id_to_pop.clear()
80
+ return output
81
+
82
+
83
+ class ActionVisualHelper(object):
84
+ def __init__(self, frame_life=20):
85
+ self.frame_life = frame_life
86
+ self.action_history = {}
87
+
88
+ def get_visualize_ids(self):
89
+ id_detected = self.check_detected()
90
+ return id_detected
91
+
92
+ def check_detected(self):
93
+ id_detected = set()
94
+ deperate_id = []
95
+ for mot_id in self.action_history:
96
+ self.action_history[mot_id]["life_remain"] -= 1
97
+ if int(self.action_history[mot_id]["class"]) == 0:
98
+ id_detected.add(mot_id)
99
+ if self.action_history[mot_id]["life_remain"] == 0:
100
+ deperate_id.append(mot_id)
101
+ for mot_id in deperate_id:
102
+ del (self.action_history[mot_id])
103
+ return id_detected
104
+
105
+ def update(self, action_res_list):
106
+ for mot_id, action_res in action_res_list:
107
+ if mot_id in self.action_history:
108
+ if int(action_res["class"]) != 0 and int(self.action_history[
109
+ mot_id]["class"]) == 0:
110
+ continue
111
+ action_info = self.action_history.get(mot_id, {})
112
+ action_info["class"] = action_res["class"]
113
+ action_info["life_remain"] = self.frame_life
114
+ self.action_history[mot_id] = action_info
pipeline/pphuman/attr_infer.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+ from functools import reduce
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import math
23
+ import paddle
24
+ from paddle.inference import Config
25
+ from paddle.inference import create_predictor
26
+
27
+ import sys
28
+ # add deploy path of PadleDetection to sys.path
29
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'])))
30
+ sys.path.insert(0, parent_path)
31
+
32
+ from python.benchmark_utils import PaddleInferBenchmark
33
+ from python.preprocess import preprocess, Resize, NormalizeImage, Permute, PadStride, LetterBoxResize, WarpAffine
34
+ from python.visualize import visualize_attr
35
+ from python.utils import argsparser, Timer, get_current_memory_mb
36
+ from python.infer import Detector, get_test_images, print_arguments, load_predictor
37
+
38
+ from PIL import Image, ImageDraw, ImageFont
39
+
40
+
41
+ class AttrDetector(Detector):
42
+ """
43
+ Args:
44
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
45
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
46
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
47
+ batch_size (int): size of pre batch in inference
48
+ trt_min_shape (int): min shape for dynamic shape in trt
49
+ trt_max_shape (int): max shape for dynamic shape in trt
50
+ trt_opt_shape (int): opt shape for dynamic shape in trt
51
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
52
+ calibration, trt_calib_mode need to set True
53
+ cpu_threads (int): cpu threads
54
+ enable_mkldnn (bool): whether to open MKLDNN
55
+ output_dir (str): The path of output
56
+ threshold (float): The threshold of score for visualization
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ model_dir,
62
+ device='CPU',
63
+ run_mode='paddle',
64
+ batch_size=1,
65
+ trt_min_shape=1,
66
+ trt_max_shape=1280,
67
+ trt_opt_shape=640,
68
+ trt_calib_mode=False,
69
+ cpu_threads=1,
70
+ enable_mkldnn=False,
71
+ output_dir='output',
72
+ threshold=0.5, ):
73
+ super(AttrDetector, self).__init__(
74
+ model_dir=model_dir,
75
+ device=device,
76
+ run_mode=run_mode,
77
+ batch_size=batch_size,
78
+ trt_min_shape=trt_min_shape,
79
+ trt_max_shape=trt_max_shape,
80
+ trt_opt_shape=trt_opt_shape,
81
+ trt_calib_mode=trt_calib_mode,
82
+ cpu_threads=cpu_threads,
83
+ enable_mkldnn=enable_mkldnn,
84
+ output_dir=output_dir,
85
+ threshold=threshold, )
86
+
87
+ @classmethod
88
+ def init_with_cfg(cls, args, cfg):
89
+ return cls(model_dir=cfg['model_dir'],
90
+ batch_size=cfg['batch_size'],
91
+ device=args.device,
92
+ run_mode=args.run_mode,
93
+ trt_min_shape=args.trt_min_shape,
94
+ trt_max_shape=args.trt_max_shape,
95
+ trt_opt_shape=args.trt_opt_shape,
96
+ trt_calib_mode=args.trt_calib_mode,
97
+ cpu_threads=args.cpu_threads,
98
+ enable_mkldnn=args.enable_mkldnn)
99
+
100
+ def get_label(self):
101
+ return self.pred_config.labels
102
+
103
+ def postprocess(self, inputs, result):
104
+ # postprocess output of predictor
105
+ im_results = result['output']
106
+
107
+ labels = self.pred_config.labels
108
+ age_list = ['AgeLess18', 'Age18-60', 'AgeOver60']
109
+ direct_list = ['Front', 'Side', 'Back']
110
+ bag_list = ['HandBag', 'ShoulderBag', 'Backpack']
111
+ upper_list = ['UpperStride', 'UpperLogo', 'UpperPlaid', 'UpperSplice']
112
+ lower_list = [
113
+ 'LowerStripe', 'LowerPattern', 'LongCoat', 'Trousers', 'Shorts',
114
+ 'Skirt&Dress'
115
+ ]
116
+ glasses_threshold = 0.3
117
+ hold_threshold = 0.6
118
+ batch_res = []
119
+ for res in im_results:
120
+ res = res.tolist()
121
+ label_res = []
122
+ # gender
123
+ gender = 'Female' if res[22] > self.threshold else 'Male'
124
+ label_res.append(gender)
125
+ # age
126
+ age = age_list[np.argmax(res[19:22])]
127
+ label_res.append(age)
128
+ # direction
129
+ direction = direct_list[np.argmax(res[23:])]
130
+ label_res.append(direction)
131
+ # glasses
132
+ glasses = 'Glasses: '
133
+ if res[1] > glasses_threshold:
134
+ glasses += 'True'
135
+ else:
136
+ glasses += 'False'
137
+ label_res.append(glasses)
138
+ # hat
139
+ hat = 'Hat: '
140
+ if res[0] > self.threshold:
141
+ hat += 'True'
142
+ else:
143
+ hat += 'False'
144
+ label_res.append(hat)
145
+ # hold obj
146
+ hold_obj = 'HoldObjectsInFront: '
147
+ if res[18] > hold_threshold:
148
+ hold_obj += 'True'
149
+ else:
150
+ hold_obj += 'False'
151
+ label_res.append(hold_obj)
152
+ # bag
153
+ bag = bag_list[np.argmax(res[15:18])]
154
+ bag_score = res[15 + np.argmax(res[15:18])]
155
+ bag_label = bag if bag_score > self.threshold else 'No bag'
156
+ label_res.append(bag_label)
157
+ # upper
158
+ upper_label = 'Upper:'
159
+ sleeve = 'LongSleeve' if res[3] > res[2] else 'ShortSleeve'
160
+ upper_label += ' {}'.format(sleeve)
161
+ upper_res = res[4:8]
162
+ if np.max(upper_res) > self.threshold:
163
+ upper_label += ' {}'.format(upper_list[np.argmax(upper_res)])
164
+ label_res.append(upper_label)
165
+ # lower
166
+ lower_res = res[8:14]
167
+ lower_label = 'Lower: '
168
+ has_lower = False
169
+ for i, l in enumerate(lower_res):
170
+ if l > self.threshold:
171
+ lower_label += ' {}'.format(lower_list[i])
172
+ has_lower = True
173
+ if not has_lower:
174
+ lower_label += ' {}'.format(lower_list[np.argmax(lower_res)])
175
+
176
+ label_res.append(lower_label)
177
+ # shoe
178
+ shoe = 'Boots' if res[14] > self.threshold else 'No boots'
179
+ label_res.append(shoe)
180
+
181
+ batch_res.append(label_res)
182
+ result = {'output': batch_res}
183
+ return result
184
+
185
+ def predict(self, repeats=1):
186
+ '''
187
+ Args:
188
+ repeats (int): repeats number for prediction
189
+ Returns:
190
+ result (dict): include 'boxes': np.ndarray: shape:[N,6], N: number of box,
191
+ matix element:[class, score, x_min, y_min, x_max, y_max]
192
+ MaskRCNN's result include 'masks': np.ndarray:
193
+ shape: [N, im_h, im_w]
194
+ '''
195
+ # model prediction
196
+ for i in range(repeats):
197
+ self.predictor.run()
198
+ output_names = self.predictor.get_output_names()
199
+ output_tensor = self.predictor.get_output_handle(output_names[0])
200
+ np_output = output_tensor.copy_to_cpu()
201
+ result = dict(output=np_output)
202
+ return result
203
+
204
+ def predict_image(self,
205
+ image_list,
206
+ run_benchmark=False,
207
+ repeats=1,
208
+ visual=True):
209
+ batch_loop_cnt = math.ceil(float(len(image_list)) / self.batch_size)
210
+ results = []
211
+ for i in range(batch_loop_cnt):
212
+ start_index = i * self.batch_size
213
+ end_index = min((i + 1) * self.batch_size, len(image_list))
214
+ batch_image_list = image_list[start_index:end_index]
215
+ if run_benchmark:
216
+ # preprocess
217
+ inputs = self.preprocess(batch_image_list) # warmup
218
+ self.det_times.preprocess_time_s.start()
219
+ inputs = self.preprocess(batch_image_list)
220
+ self.det_times.preprocess_time_s.end()
221
+
222
+ # model prediction
223
+ result = self.predict(repeats=repeats) # warmup
224
+ self.det_times.inference_time_s.start()
225
+ result = self.predict(repeats=repeats)
226
+ self.det_times.inference_time_s.end(repeats=repeats)
227
+
228
+ # postprocess
229
+ result_warmup = self.postprocess(inputs, result) # warmup
230
+ self.det_times.postprocess_time_s.start()
231
+ result = self.postprocess(inputs, result)
232
+ self.det_times.postprocess_time_s.end()
233
+ self.det_times.img_num += len(batch_image_list)
234
+
235
+ cm, gm, gu = get_current_memory_mb()
236
+ self.cpu_mem += cm
237
+ self.gpu_mem += gm
238
+ self.gpu_util += gu
239
+ else:
240
+ # preprocess
241
+ self.det_times.preprocess_time_s.start()
242
+ inputs = self.preprocess(batch_image_list)
243
+ self.det_times.preprocess_time_s.end()
244
+
245
+ # model prediction
246
+ self.det_times.inference_time_s.start()
247
+ result = self.predict()
248
+ self.det_times.inference_time_s.end()
249
+
250
+ # postprocess
251
+ self.det_times.postprocess_time_s.start()
252
+ result = self.postprocess(inputs, result)
253
+ self.det_times.postprocess_time_s.end()
254
+ self.det_times.img_num += len(batch_image_list)
255
+
256
+ if visual:
257
+ visualize(
258
+ batch_image_list, result, output_dir=self.output_dir)
259
+
260
+ results.append(result)
261
+ if visual:
262
+ print('Test iter {}'.format(i))
263
+
264
+ results = self.merge_batch_result(results)
265
+ return results
266
+
267
+ def merge_batch_result(self, batch_result):
268
+ if len(batch_result) == 1:
269
+ return batch_result[0]
270
+ res_key = batch_result[0].keys()
271
+ results = {k: [] for k in res_key}
272
+ for res in batch_result:
273
+ for k, v in res.items():
274
+ results[k].extend(v)
275
+ return results
276
+
277
+
278
+ def visualize(image_list, batch_res, output_dir='output'):
279
+
280
+ # visualize the predict result
281
+ batch_res = batch_res['output']
282
+ for image_file, res in zip(image_list, batch_res):
283
+ im = visualize_attr(image_file, [res])
284
+ if not os.path.exists(output_dir):
285
+ os.makedirs(output_dir)
286
+ img_name = os.path.split(image_file)[-1]
287
+ out_path = os.path.join(output_dir, img_name)
288
+ cv2.imwrite(out_path, im)
289
+ print("save result to: " + out_path)
290
+
291
+
292
+ def main():
293
+ detector = AttrDetector(
294
+ FLAGS.model_dir,
295
+ device=FLAGS.device,
296
+ run_mode=FLAGS.run_mode,
297
+ batch_size=FLAGS.batch_size,
298
+ trt_min_shape=FLAGS.trt_min_shape,
299
+ trt_max_shape=FLAGS.trt_max_shape,
300
+ trt_opt_shape=FLAGS.trt_opt_shape,
301
+ trt_calib_mode=FLAGS.trt_calib_mode,
302
+ cpu_threads=FLAGS.cpu_threads,
303
+ enable_mkldnn=FLAGS.enable_mkldnn,
304
+ threshold=FLAGS.threshold,
305
+ output_dir=FLAGS.output_dir)
306
+
307
+ # predict from image
308
+ if FLAGS.image_dir is None and FLAGS.image_file is not None:
309
+ assert FLAGS.batch_size == 1, "batch_size should be 1, when image_file is not None"
310
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
311
+ detector.predict_image(img_list, FLAGS.run_benchmark, repeats=10)
312
+ if not FLAGS.run_benchmark:
313
+ detector.det_times.info(average=True)
314
+ else:
315
+ mems = {
316
+ 'cpu_rss_mb': detector.cpu_mem / len(img_list),
317
+ 'gpu_rss_mb': detector.gpu_mem / len(img_list),
318
+ 'gpu_util': detector.gpu_util * 100 / len(img_list)
319
+ }
320
+
321
+ perf_info = detector.det_times.report(average=True)
322
+ model_dir = FLAGS.model_dir
323
+ mode = FLAGS.run_mode
324
+ model_info = {
325
+ 'model_name': model_dir.strip('/').split('/')[-1],
326
+ 'precision': mode.split('_')[-1]
327
+ }
328
+ data_info = {
329
+ 'batch_size': FLAGS.batch_size,
330
+ 'shape': "dynamic_shape",
331
+ 'data_num': perf_info['img_num']
332
+ }
333
+ det_log = PaddleInferBenchmark(detector.config, model_info, data_info,
334
+ perf_info, mems)
335
+ det_log('Attr')
336
+
337
+
338
+ if __name__ == '__main__':
339
+ paddle.enable_static()
340
+ parser = argsparser()
341
+ FLAGS = parser.parse_args()
342
+ print_arguments(FLAGS)
343
+ FLAGS.device = FLAGS.device.upper()
344
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
345
+ ], "device should be CPU, GPU or XPU"
346
+ assert not FLAGS.use_gpu, "use_gpu has been deprecated, please use --device"
347
+
348
+ main()
pipeline/pphuman/mtmct.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pptracking.python.mot.visualize import plot_tracking
16
+ from python.visualize import visualize_attr
17
+ import os
18
+ import re
19
+ import cv2
20
+ import gc
21
+ import numpy as np
22
+ try:
23
+ from sklearn import preprocessing
24
+ from sklearn.cluster import AgglomerativeClustering
25
+ except:
26
+ print(
27
+ 'Warning: Unable to use MTMCT in PP-Human, please install sklearn, for example: `pip install sklearn`'
28
+ )
29
+ pass
30
+ import pandas as pd
31
+ from tqdm import tqdm
32
+ from functools import reduce
33
+ import warnings
34
+ warnings.filterwarnings("ignore")
35
+
36
+
37
+ def gen_restxt(output_dir_filename, map_tid, cid_tid_dict):
38
+ pattern = re.compile(r'c(\d)_t(\d)')
39
+ f_w = open(output_dir_filename, 'w')
40
+ for key, res in cid_tid_dict.items():
41
+ cid, tid = pattern.search(key).groups()
42
+ cid = int(cid) + 1
43
+ rects = res["rects"]
44
+ frames = res["frames"]
45
+ for idx, bbox in enumerate(rects):
46
+ bbox[0][3:] -= bbox[0][1:3]
47
+ fid = frames[idx] + 1
48
+ rect = [max(int(x), 0) for x in bbox[0][1:]]
49
+ if key in map_tid:
50
+ new_tid = map_tid[key]
51
+ f_w.write(
52
+ str(cid) + ' ' + str(new_tid) + ' ' + str(fid) + ' ' +
53
+ ' '.join(map(str, rect)) + '\n')
54
+ print('gen_res: write file in {}'.format(output_dir_filename))
55
+ f_w.close()
56
+
57
+
58
+ def get_mtmct_matching_results(pred_mtmct_file,
59
+ secs_interval=0.5,
60
+ video_fps=20):
61
+ res = np.loadtxt(pred_mtmct_file) # 'cid, tid, fid, x1, y1, w, h, -1, -1'
62
+ camera_ids = list(map(int, np.unique(res[:, 0])))
63
+
64
+ res = res[:, :7]
65
+ # each line in res: 'cid, tid, fid, x1, y1, w, h'
66
+
67
+ camera_tids = []
68
+ camera_results = dict()
69
+ for c_id in camera_ids:
70
+ camera_results[c_id] = res[res[:, 0] == c_id]
71
+ tids = np.unique(camera_results[c_id][:, 1])
72
+ tids = list(map(int, tids))
73
+ camera_tids.append(tids)
74
+
75
+ # select common tids throughout each video
76
+ common_tids = reduce(np.intersect1d, camera_tids)
77
+
78
+ # get mtmct matching results by cid_tid_fid_results[c_id][t_id][f_id]
79
+ cid_tid_fid_results = dict()
80
+ cid_tid_to_fids = dict()
81
+ interval = int(secs_interval * video_fps) # preferably less than 10
82
+ for c_id in camera_ids:
83
+ cid_tid_fid_results[c_id] = dict()
84
+ cid_tid_to_fids[c_id] = dict()
85
+ for t_id in common_tids:
86
+ tid_mask = camera_results[c_id][:, 1] == t_id
87
+ cid_tid_fid_results[c_id][t_id] = dict()
88
+
89
+ camera_trackid_results = camera_results[c_id][tid_mask]
90
+ fids = np.unique(camera_trackid_results[:, 2])
91
+ fids = fids[fids % interval == 0]
92
+ fids = list(map(int, fids))
93
+ cid_tid_to_fids[c_id][t_id] = fids
94
+
95
+ for f_id in fids:
96
+ st_frame = f_id
97
+ ed_frame = f_id + interval
98
+
99
+ st_mask = camera_trackid_results[:, 2] >= st_frame
100
+ ed_mask = camera_trackid_results[:, 2] < ed_frame
101
+ frame_mask = np.logical_and(st_mask, ed_mask)
102
+ cid_tid_fid_results[c_id][t_id][f_id] = camera_trackid_results[
103
+ frame_mask]
104
+
105
+ return camera_results, cid_tid_fid_results
106
+
107
+
108
+ def save_mtmct_vis_results(camera_results,
109
+ captures,
110
+ output_dir,
111
+ multi_res=None):
112
+ # camera_results: 'cid, tid, fid, x1, y1, w, h'
113
+ camera_ids = list(camera_results.keys())
114
+
115
+ import shutil
116
+ save_dir = os.path.join(output_dir, 'mtmct_vis')
117
+ if os.path.exists(save_dir):
118
+ shutil.rmtree(save_dir)
119
+ os.makedirs(save_dir)
120
+
121
+ for idx, video_file in enumerate(captures):
122
+ capture = cv2.VideoCapture(video_file)
123
+ cid = camera_ids[idx]
124
+ basename = os.path.basename(video_file)
125
+ video_out_name = "vis_" + basename
126
+ out_path = os.path.join(save_dir, video_out_name)
127
+ print("Start visualizing output video: {}".format(out_path))
128
+
129
+ # Get Video info : resolution, fps, frame count
130
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
131
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
132
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
133
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
134
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
135
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
136
+ frame_id = 0
137
+ while (1):
138
+ if frame_id % 50 == 0:
139
+ print('frame id: ', frame_id)
140
+ ret, frame = capture.read()
141
+ frame_id += 1
142
+ if not ret:
143
+ if frame_id == 1:
144
+ print("video read failed!")
145
+ break
146
+ frame_results = camera_results[cid][camera_results[cid][:, 2] ==
147
+ frame_id]
148
+ boxes = frame_results[:, -4:]
149
+ ids = frame_results[:, 1]
150
+ image = plot_tracking(
151
+ frame, boxes, ids, frame_id=frame_id, fps=fps)
152
+
153
+ # add attr vis
154
+ if multi_res:
155
+ tid_list = multi_res.keys() # c0_t1, c0_t2...
156
+ all_attr_result = [multi_res[i]["attrs"]
157
+ for i in tid_list] # all cid_tid result
158
+ if any(
159
+ all_attr_result
160
+ ): # at least one cid_tid[attrs] is not None will goes to attrs_vis
161
+ attr_res = []
162
+ for k in tid_list:
163
+ if (frame_id - 1) >= len(multi_res[k]['attrs']):
164
+ t_attr = None
165
+ else:
166
+ t_attr = multi_res[k]['attrs'][frame_id - 1]
167
+ attr_res.append(t_attr)
168
+ image = visualize_attr(
169
+ image, attr_res, boxes, is_mtmct=True)
170
+
171
+ writer.write(image)
172
+ writer.release()
173
+
174
+
175
+ def get_euclidean(x, y, **kwargs):
176
+ m = x.shape[0]
177
+ n = y.shape[0]
178
+ distmat = (np.power(x, 2).sum(axis=1, keepdims=True).repeat(
179
+ n, axis=1) + np.power(y, 2).sum(axis=1, keepdims=True).repeat(
180
+ m, axis=1).T)
181
+ distmat -= np.dot(2 * x, y.T)
182
+ return distmat
183
+
184
+
185
+ def cosine_similarity(x, y, eps=1e-12):
186
+ """
187
+ Computes cosine similarity between two tensors.
188
+ Value == 1 means the same vector
189
+ Value == 0 means perpendicular vectors
190
+ """
191
+ x_n, y_n = np.linalg.norm(
192
+ x, axis=1, keepdims=True), np.linalg.norm(
193
+ y, axis=1, keepdims=True)
194
+ x_norm = x / np.maximum(x_n, eps * np.ones_like(x_n))
195
+ y_norm = y / np.maximum(y_n, eps * np.ones_like(y_n))
196
+ sim_mt = np.dot(x_norm, y_norm.T)
197
+ return sim_mt
198
+
199
+
200
+ def get_cosine(x, y, eps=1e-12):
201
+ """
202
+ Computes cosine distance between two tensors.
203
+ The cosine distance is the inverse cosine similarity
204
+ -> cosine_distance = abs(-cosine_distance) to make it
205
+ similar in behaviour to euclidean distance
206
+ """
207
+ sim_mt = cosine_similarity(x, y, eps)
208
+ return sim_mt
209
+
210
+
211
+ def get_dist_mat(x, y, func_name="euclidean"):
212
+ if func_name == "cosine":
213
+ dist_mat = get_cosine(x, y)
214
+ elif func_name == "euclidean":
215
+ dist_mat = get_euclidean(x, y)
216
+ print("Using {} as distance function during evaluation".format(func_name))
217
+ return dist_mat
218
+
219
+
220
+ def intracam_ignore(st_mask, cid_tids):
221
+ count = len(cid_tids)
222
+ for i in range(count):
223
+ for j in range(count):
224
+ if cid_tids[i][1] == cid_tids[j][1]:
225
+ st_mask[i, j] = 0.
226
+ return st_mask
227
+
228
+
229
+ def get_sim_matrix_new(cid_tid_dict, cid_tids):
230
+ # Note: camera independent get_sim_matrix function,
231
+ # which is different from the one in camera_utils.py.
232
+ count = len(cid_tids)
233
+
234
+ q_arr = np.array(
235
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
236
+ g_arr = np.array(
237
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
238
+ #compute distmat
239
+ distmat = get_dist_mat(q_arr, g_arr, func_name="cosine")
240
+
241
+ #mask the element which belongs to same video
242
+ st_mask = np.ones((count, count), dtype=np.float32)
243
+ st_mask = intracam_ignore(st_mask, cid_tids)
244
+
245
+ sim_matrix = distmat * st_mask
246
+ np.fill_diagonal(sim_matrix, 0.)
247
+ return 1. - sim_matrix
248
+
249
+
250
+ def get_match(cluster_labels):
251
+ cluster_dict = dict()
252
+ cluster = list()
253
+ for i, l in enumerate(cluster_labels):
254
+ if l in list(cluster_dict.keys()):
255
+ cluster_dict[l].append(i)
256
+ else:
257
+ cluster_dict[l] = [i]
258
+ for idx in cluster_dict:
259
+ cluster.append(cluster_dict[idx])
260
+ return cluster
261
+
262
+
263
+ def get_cid_tid(cluster_labels, cid_tids):
264
+ cluster = list()
265
+ for labels in cluster_labels:
266
+ cid_tid_list = list()
267
+ for label in labels:
268
+ cid_tid_list.append(cid_tids[label])
269
+ cluster.append(cid_tid_list)
270
+ return cluster
271
+
272
+
273
+ def get_labels(cid_tid_dict, cid_tids):
274
+ #compute cost matrix between features
275
+ cost_matrix = get_sim_matrix_new(cid_tid_dict, cid_tids)
276
+
277
+ #cluster all the features
278
+ cluster1 = AgglomerativeClustering(
279
+ n_clusters=None,
280
+ distance_threshold=0.5,
281
+ affinity='precomputed',
282
+ linkage='complete')
283
+ cluster_labels1 = cluster1.fit_predict(cost_matrix)
284
+ labels = get_match(cluster_labels1)
285
+
286
+ sub_cluster = get_cid_tid(labels, cid_tids)
287
+ return labels
288
+
289
+
290
+ def sub_cluster(cid_tid_dict):
291
+ '''
292
+ cid_tid_dict: all camera_id and track_id
293
+ '''
294
+ #get all keys
295
+ cid_tids = sorted([key for key in cid_tid_dict.keys()])
296
+
297
+ #cluster all trackid
298
+ clu = get_labels(cid_tid_dict, cid_tids)
299
+
300
+ #relabel every cluster groups
301
+ new_clu = list()
302
+ for c_list in clu:
303
+ new_clu.append([cid_tids[c] for c in c_list])
304
+ cid_tid_label = dict()
305
+ for i, c_list in enumerate(new_clu):
306
+ for c in c_list:
307
+ cid_tid_label[c] = i + 1
308
+ return cid_tid_label
309
+
310
+
311
+ def distill_idfeat(mot_res):
312
+ qualities_list = mot_res["qualities"]
313
+ feature_list = mot_res["features"]
314
+ rects = mot_res["rects"]
315
+
316
+ qualities_new = []
317
+ feature_new = []
318
+ #filter rect less than 100*20
319
+ for idx, rect in enumerate(rects):
320
+ conf, xmin, ymin, xmax, ymax = rect[0]
321
+ if (xmax - xmin) * (ymax - ymin) and (xmax > xmin) > 2000:
322
+ qualities_new.append(qualities_list[idx])
323
+ feature_new.append(feature_list[idx])
324
+ #take all features if available rect is less than 2
325
+ if len(qualities_new) < 2:
326
+ qualities_new = qualities_list
327
+ feature_new = feature_list
328
+
329
+ #if available frames number is more than 200, take one frame data per 20 frames
330
+ skipf = 1
331
+ if len(qualities_new) > 20:
332
+ skipf = 2
333
+ quality_skip = np.array(qualities_new[::skipf])
334
+ feature_skip = np.array(feature_new[::skipf])
335
+
336
+ #sort features with image qualities, take the most trustworth features
337
+ topk_argq = np.argsort(quality_skip)[::-1]
338
+ if (quality_skip > 0.6).sum() > 1:
339
+ topk_feat = feature_skip[topk_argq[quality_skip > 0.6]]
340
+ else:
341
+ topk_feat = feature_skip[topk_argq]
342
+
343
+ #get final features by mean or cluster, at most take five
344
+ mean_feat = np.mean(topk_feat[:5], axis=0)
345
+ return mean_feat
346
+
347
+
348
+ def res2dict(multi_res):
349
+ cid_tid_dict = {}
350
+ for cid, c_res in enumerate(multi_res):
351
+ for tid, res in c_res.items():
352
+ key = "c" + str(cid) + "_t" + str(tid)
353
+ if key not in cid_tid_dict:
354
+ if len(res["features"]) == 0:
355
+ continue
356
+ cid_tid_dict[key] = res
357
+ cid_tid_dict[key]['mean_feat'] = distill_idfeat(res)
358
+ return cid_tid_dict
359
+
360
+
361
+ def mtmct_process(multi_res, captures, mtmct_vis=True, output_dir="output"):
362
+ cid_tid_dict = res2dict(multi_res)
363
+ if len(cid_tid_dict) == 0:
364
+ print("no tracking result found, mtmct will be skiped.")
365
+ return
366
+ map_tid = sub_cluster(cid_tid_dict)
367
+
368
+ if not os.path.exists(output_dir):
369
+ os.mkdir(output_dir)
370
+ pred_mtmct_file = os.path.join(output_dir, 'mtmct_result.txt')
371
+ gen_restxt(pred_mtmct_file, map_tid, cid_tid_dict)
372
+
373
+ if mtmct_vis:
374
+ camera_results, cid_tid_fid_res = get_mtmct_matching_results(
375
+ pred_mtmct_file)
376
+
377
+ save_mtmct_vis_results(
378
+ camera_results,
379
+ captures,
380
+ output_dir=output_dir,
381
+ multi_res=cid_tid_dict)
pipeline/pphuman/reid.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import sys
17
+ import cv2
18
+ import numpy as np
19
+ # add deploy path of PadleDetection to sys.path
20
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2)))
21
+ sys.path.insert(0, parent_path)
22
+
23
+ from python.infer import PredictConfig
24
+ from pptracking.python.det_infer import load_predictor
25
+ from python.utils import Timer
26
+
27
+
28
+ class ReID(object):
29
+ """
30
+ ReID of SDE methods
31
+
32
+ Args:
33
+ pred_config (object): config of model, defined by `Config(model_dir)`
34
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
35
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
36
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
37
+ batch_size (int): size of per batch in inference, default 50 means at most
38
+ 50 sub images can be made a batch and send into ReID model
39
+ trt_min_shape (int): min shape for dynamic shape in trt
40
+ trt_max_shape (int): max shape for dynamic shape in trt
41
+ trt_opt_shape (int): opt shape for dynamic shape in trt
42
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
43
+ calibration, trt_calib_mode need to set True
44
+ cpu_threads (int): cpu threads
45
+ enable_mkldnn (bool): whether to open MKLDNN
46
+ """
47
+
48
+ def __init__(self,
49
+ model_dir,
50
+ device='CPU',
51
+ run_mode='paddle',
52
+ batch_size=50,
53
+ trt_min_shape=1,
54
+ trt_max_shape=1088,
55
+ trt_opt_shape=608,
56
+ trt_calib_mode=False,
57
+ cpu_threads=4,
58
+ enable_mkldnn=False):
59
+ self.pred_config = self.set_config(model_dir)
60
+ self.predictor, self.config = load_predictor(
61
+ model_dir,
62
+ run_mode=run_mode,
63
+ batch_size=batch_size,
64
+ min_subgraph_size=self.pred_config.min_subgraph_size,
65
+ device=device,
66
+ use_dynamic_shape=self.pred_config.use_dynamic_shape,
67
+ trt_min_shape=trt_min_shape,
68
+ trt_max_shape=trt_max_shape,
69
+ trt_opt_shape=trt_opt_shape,
70
+ trt_calib_mode=trt_calib_mode,
71
+ cpu_threads=cpu_threads,
72
+ enable_mkldnn=enable_mkldnn)
73
+ self.det_times = Timer()
74
+ self.cpu_mem, self.gpu_mem, self.gpu_util = 0, 0, 0
75
+ self.batch_size = batch_size
76
+ self.input_wh = (128, 256)
77
+
78
+ @classmethod
79
+ def init_with_cfg(cls, args, cfg):
80
+ return cls(model_dir=cfg['model_dir'],
81
+ batch_size=cfg['batch_size'],
82
+ device=args.device,
83
+ run_mode=args.run_mode,
84
+ trt_min_shape=args.trt_min_shape,
85
+ trt_max_shape=args.trt_max_shape,
86
+ trt_opt_shape=args.trt_opt_shape,
87
+ trt_calib_mode=args.trt_calib_mode,
88
+ cpu_threads=args.cpu_threads,
89
+ enable_mkldnn=args.enable_mkldnn)
90
+
91
+ def set_config(self, model_dir):
92
+ return PredictConfig(model_dir)
93
+
94
+ def check_img_quality(self, crop, bbox, xyxy):
95
+ if crop is None:
96
+ return None
97
+ #eclipse
98
+ eclipse_quality = 1.0
99
+ inner_rect = np.zeros(xyxy.shape)
100
+ inner_rect[:, :2] = np.maximum(xyxy[:, :2], bbox[None, :2])
101
+ inner_rect[:, 2:] = np.minimum(xyxy[:, 2:], bbox[None, 2:])
102
+ wh_array = inner_rect[:, 2:] - inner_rect[:, :2]
103
+ filt = np.logical_and(wh_array[:, 0] > 0, wh_array[:, 1] > 0)
104
+ wh_array = wh_array[filt]
105
+ if wh_array.shape[0] > 1:
106
+ eclipse_ratio = wh_array / (bbox[2:] - bbox[:2])
107
+ eclipse_area_ratio = eclipse_ratio[:, 0] * eclipse_ratio[:, 1]
108
+ ear_lst = eclipse_area_ratio.tolist()
109
+ ear_lst.sort(reverse=True)
110
+ eclipse_quality = 1.0 - ear_lst[1]
111
+ bbox_wh = (bbox[2:] - bbox[:2])
112
+ height_quality = bbox_wh[1] / (bbox_wh[0] * 2)
113
+ eclipse_quality = min(eclipse_quality, height_quality)
114
+
115
+ #definition
116
+ cropgray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
117
+ definition = int(cv2.Laplacian(cropgray, cv2.CV_64F, ksize=3).var())
118
+ brightness = int(cropgray.mean())
119
+ bd_quality = min(1., brightness / 50.)
120
+
121
+ eclipse_weight = 0.7
122
+ return eclipse_quality * eclipse_weight + bd_quality * (1 -
123
+ eclipse_weight)
124
+
125
+ def normal_crop(self, image, rect):
126
+ imgh, imgw, c = image.shape
127
+ label, conf, xmin, ymin, xmax, ymax = [int(x) for x in rect.tolist()]
128
+ xmin = max(0, xmin)
129
+ ymin = max(0, ymin)
130
+ xmax = min(imgw, xmax)
131
+ ymax = min(imgh, ymax)
132
+ if label != 0 or xmax <= xmin or ymax <= ymin:
133
+ print("Warning! label missed!!")
134
+ return None, None, None
135
+ return image[ymin:ymax, xmin:xmax, :]
136
+
137
+ def crop_image_with_mot(self, image, mot_res):
138
+ res = mot_res['boxes']
139
+ crop_res = []
140
+ img_quality = []
141
+ rects = []
142
+ for box in res:
143
+ crop_image = self.normal_crop(image, box[1:])
144
+ quality_item = self.check_img_quality(crop_image, box[3:],
145
+ res[:, 3:])
146
+ if crop_image is not None:
147
+ crop_res.append(crop_image)
148
+ img_quality.append(quality_item)
149
+ rects.append(box)
150
+ return crop_res, img_quality, rects
151
+
152
+ def preprocess(self,
153
+ imgs,
154
+ mean=[0.485, 0.456, 0.406],
155
+ std=[0.229, 0.224, 0.225]):
156
+ im_batch = []
157
+ for img in imgs:
158
+ img = cv2.resize(img, self.input_wh)
159
+ img = img.astype('float32') / 255.
160
+ img -= np.array(mean)
161
+ img /= np.array(std)
162
+ im_batch.append(img.transpose((2, 0, 1)))
163
+ inputs = {}
164
+ inputs['x'] = np.array(im_batch).astype('float32')
165
+ return inputs
166
+
167
+ def predict(self, crops, repeats=1, add_timer=True, seq_name=''):
168
+ # preprocess
169
+ if add_timer:
170
+ self.det_times.preprocess_time_s.start()
171
+ inputs = self.preprocess(crops)
172
+ input_names = self.predictor.get_input_names()
173
+ for i in range(len(input_names)):
174
+ input_tensor = self.predictor.get_input_handle(input_names[i])
175
+ input_tensor.copy_from_cpu(inputs[input_names[i]])
176
+
177
+ if add_timer:
178
+ self.det_times.preprocess_time_s.end()
179
+ self.det_times.inference_time_s.start()
180
+
181
+ # model prediction
182
+ for i in range(repeats):
183
+ self.predictor.run()
184
+ output_names = self.predictor.get_output_names()
185
+ feature_tensor = self.predictor.get_output_handle(output_names[0])
186
+ pred_embs = feature_tensor.copy_to_cpu()
187
+ if add_timer:
188
+ self.det_times.inference_time_s.end(repeats=repeats)
189
+ self.det_times.postprocess_time_s.start()
190
+
191
+ if add_timer:
192
+ self.det_times.postprocess_time_s.end()
193
+ self.det_times.img_num += 1
194
+ return pred_embs
195
+
196
+ def predict_batch(self, imgs, batch_size=4):
197
+ batch_feat = []
198
+ for b in range(0, len(imgs), batch_size):
199
+ b_end = min(len(imgs), b + batch_size)
200
+ batch_imgs = imgs[b:b_end]
201
+ feat = self.predict(batch_imgs)
202
+ batch_feat.extend(feat.tolist())
203
+
204
+ return batch_feat
pipeline/pphuman/video_action_infer.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+
19
+ import cv2
20
+ import numpy as np
21
+ import math
22
+ import paddle
23
+ import sys
24
+ import paddle.nn.functional as F
25
+
26
+ # add deploy path of PadleDetection to sys.path
27
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2)))
28
+ sys.path.insert(0, parent_path)
29
+
30
+ from paddle.inference import Config, create_predictor
31
+ from python.utils import argsparser, Timer, get_current_memory_mb
32
+ from python.benchmark_utils import PaddleInferBenchmark
33
+ from python.infer import Detector, print_arguments
34
+ from video_action_preprocess import VideoDecoder, Sampler, Scale, CenterCrop, Normalization, Image2Array
35
+
36
+
37
+ def softmax(x):
38
+ f_x = np.exp(x) / np.sum(np.exp(x))
39
+ return f_x
40
+
41
+
42
+ class VideoActionRecognizer(object):
43
+ """
44
+ Args:
45
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
46
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
47
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
48
+ batch_size (int): size of pre batch in inference
49
+ trt_min_shape (int): min shape for dynamic shape in trt
50
+ trt_max_shape (int): max shape for dynamic shape in trt
51
+ trt_opt_shape (int): opt shape for dynamic shape in trt
52
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
53
+ calibration, trt_calib_mode need to set True
54
+ cpu_threads (int): cpu threads
55
+ enable_mkldnn (bool): whether to open MKLDNN
56
+ """
57
+
58
+ def __init__(self,
59
+ model_dir,
60
+ device='CPU',
61
+ run_mode='paddle',
62
+ num_seg=8,
63
+ seg_len=1,
64
+ short_size=256,
65
+ target_size=224,
66
+ top_k=1,
67
+ batch_size=1,
68
+ trt_min_shape=1,
69
+ trt_max_shape=1280,
70
+ trt_opt_shape=640,
71
+ trt_calib_mode=False,
72
+ cpu_threads=1,
73
+ enable_mkldnn=False,
74
+ ir_optim=True):
75
+
76
+ self.num_seg = num_seg
77
+ self.seg_len = seg_len
78
+ self.short_size = short_size
79
+ self.target_size = target_size
80
+ self.top_k = top_k
81
+
82
+ assert batch_size == 1, "VideoActionRecognizer only support batch_size=1 now."
83
+
84
+ self.model_dir = model_dir
85
+ self.device = device
86
+ self.run_mode = run_mode
87
+ self.batch_size = batch_size
88
+ self.trt_min_shape = trt_min_shape
89
+ self.trt_max_shape = trt_max_shape
90
+ self.trt_opt_shape = trt_opt_shape
91
+ self.trt_calib_mode = trt_calib_mode
92
+ self.cpu_threads = cpu_threads
93
+ self.enable_mkldnn = enable_mkldnn
94
+ self.ir_optim = ir_optim
95
+
96
+ self.recognize_times = Timer()
97
+
98
+ model_file_path = glob.glob(os.path.join(model_dir, "*.pdmodel"))[0]
99
+ params_file_path = glob.glob(os.path.join(model_dir, "*.pdiparams"))[0]
100
+ self.config = Config(model_file_path, params_file_path)
101
+
102
+ if device == "GPU" or device == "gpu":
103
+ self.config.enable_use_gpu(8000, 0)
104
+ else:
105
+ self.config.disable_gpu()
106
+ if self.enable_mkldnn:
107
+ # cache 10 different shapes for mkldnn to avoid memory leak
108
+ self.config.set_mkldnn_cache_capacity(10)
109
+ self.config.enable_mkldnn()
110
+
111
+ self.config.switch_ir_optim(self.ir_optim) # default true
112
+
113
+ precision_map = {
114
+ 'trt_int8': Config.Precision.Int8,
115
+ 'trt_fp32': Config.Precision.Float32,
116
+ 'trt_fp16': Config.Precision.Half
117
+ }
118
+ if run_mode in precision_map.keys():
119
+ self.config.enable_tensorrt_engine(
120
+ max_batch_size=8, precision_mode=precision_map[run_mode])
121
+
122
+ self.config.enable_memory_optim()
123
+ # use zero copy
124
+ self.config.switch_use_feed_fetch_ops(False)
125
+
126
+ self.predictor = create_predictor(self.config)
127
+
128
+ @classmethod
129
+ def init_with_cfg(cls, args, cfg):
130
+ return cls(model_dir=cfg['model_dir'],
131
+ short_size=cfg['short_size'],
132
+ target_size=cfg['target_size'],
133
+ batch_size=cfg['batch_size'],
134
+ device=args.device,
135
+ run_mode=args.run_mode,
136
+ trt_min_shape=args.trt_min_shape,
137
+ trt_max_shape=args.trt_max_shape,
138
+ trt_opt_shape=args.trt_opt_shape,
139
+ trt_calib_mode=args.trt_calib_mode,
140
+ cpu_threads=args.cpu_threads,
141
+ enable_mkldnn=args.enable_mkldnn)
142
+
143
+ def preprocess_batch(self, file_list):
144
+ batched_inputs = []
145
+ for file in file_list:
146
+ inputs = self.preprocess(file)
147
+ batched_inputs.append(inputs)
148
+ batched_inputs = [
149
+ np.concatenate([item[i] for item in batched_inputs])
150
+ for i in range(len(batched_inputs[0]))
151
+ ]
152
+ self.input_file = file_list
153
+ return batched_inputs
154
+
155
+ def get_timer(self):
156
+ return self.recognize_times
157
+
158
+ def predict(self, input):
159
+ '''
160
+ Args:
161
+ input (str) or (list): video file path or image data list
162
+ Returns:
163
+ results (dict):
164
+ '''
165
+
166
+ input_names = self.predictor.get_input_names()
167
+ input_tensor = self.predictor.get_input_handle(input_names[0])
168
+
169
+ output_names = self.predictor.get_output_names()
170
+ output_tensor = self.predictor.get_output_handle(output_names[0])
171
+
172
+ # preprocess
173
+ self.recognize_times.preprocess_time_s.start()
174
+ if type(input) == str:
175
+ inputs = self.preprocess_video(input)
176
+ else:
177
+ inputs = self.preprocess_frames(input)
178
+ self.recognize_times.preprocess_time_s.end()
179
+
180
+ inputs = np.expand_dims(
181
+ inputs, axis=0).repeat(
182
+ self.batch_size, axis=0).copy()
183
+
184
+ input_tensor.copy_from_cpu(inputs)
185
+
186
+ # model prediction
187
+ self.recognize_times.inference_time_s.start()
188
+ self.predictor.run()
189
+ self.recognize_times.inference_time_s.end()
190
+
191
+ output = output_tensor.copy_to_cpu()
192
+
193
+ # postprocess
194
+ self.recognize_times.postprocess_time_s.start()
195
+ classes, scores = self.postprocess(output)
196
+ self.recognize_times.postprocess_time_s.end()
197
+
198
+ return classes, scores
199
+
200
+ def preprocess_frames(self, frame_list):
201
+ """
202
+ frame_list: list, frame list
203
+ return: list
204
+ """
205
+
206
+ results = {}
207
+ results['frames_len'] = len(frame_list)
208
+ results["imgs"] = frame_list
209
+
210
+ img_mean = [0.485, 0.456, 0.406]
211
+ img_std = [0.229, 0.224, 0.225]
212
+ ops = [
213
+ CenterCrop(self.target_size), Image2Array(),
214
+ Normalization(img_mean, img_std)
215
+ ]
216
+ for op in ops:
217
+ results = op(results)
218
+
219
+ res = np.expand_dims(results['imgs'], axis=0).copy()
220
+ return [res]
221
+
222
+ def preprocess_video(self, input_file):
223
+ """
224
+ input_file: str, file path
225
+ return: list
226
+ """
227
+ assert os.path.isfile(input_file) is not None, "{0} not exists".format(
228
+ input_file)
229
+
230
+ results = {'filename': input_file}
231
+ img_mean = [0.485, 0.456, 0.406]
232
+ img_std = [0.229, 0.224, 0.225]
233
+ ops = [
234
+ VideoDecoder(), Sampler(
235
+ self.num_seg, self.seg_len, valid_mode=True),
236
+ Scale(self.short_size), CenterCrop(self.target_size),
237
+ Image2Array(), Normalization(img_mean, img_std)
238
+ ]
239
+ for op in ops:
240
+ results = op(results)
241
+
242
+ res = np.expand_dims(results['imgs'], axis=0).copy()
243
+ return [res]
244
+
245
+ def postprocess(self, output):
246
+ output = output.flatten() # numpy.ndarray
247
+ output = softmax(output)
248
+ classes = np.argpartition(output, -self.top_k)[-self.top_k:]
249
+ classes = classes[np.argsort(-output[classes])]
250
+ scores = output[classes]
251
+ return classes, scores
252
+
253
+
254
+ def main():
255
+ if not FLAGS.run_benchmark:
256
+ assert FLAGS.batch_size == 1
257
+ assert FLAGS.use_fp16 is False
258
+ else:
259
+ assert FLAGS.use_gpu is True
260
+
261
+ recognizer = VideoActionRecognizer(
262
+ FLAGS.model_dir,
263
+ short_size=FLAGS.short_size,
264
+ target_size=FLAGS.target_size,
265
+ device=FLAGS.device,
266
+ run_mode=FLAGS.run_mode,
267
+ batch_size=FLAGS.batch_size,
268
+ trt_min_shape=FLAGS.trt_min_shape,
269
+ trt_max_shape=FLAGS.trt_max_shape,
270
+ trt_opt_shape=FLAGS.trt_opt_shape,
271
+ trt_calib_mode=FLAGS.trt_calib_mode,
272
+ cpu_threads=FLAGS.cpu_threads,
273
+ enable_mkldnn=FLAGS.enable_mkldnn, )
274
+
275
+ if not FLAGS.run_benchmark:
276
+ classes, scores = recognizer.predict(FLAGS.video_file)
277
+ print("Current video file: {}".format(FLAGS.video_file))
278
+ print("\ttop-1 class: {0}".format(classes[0]))
279
+ print("\ttop-1 score: {0}".format(scores[0]))
280
+ else:
281
+ cm, gm, gu = get_current_memory_mb()
282
+ mems = {'cpu_rss_mb': cm, 'gpu_rss_mb': gm, 'gpu_util': gu * 100}
283
+
284
+ perf_info = recognizer.recognize_times.report()
285
+ model_dir = FLAGS.model_dir
286
+ mode = FLAGS.run_mode
287
+ model_info = {
288
+ 'model_name': model_dir.strip('/').split('/')[-1],
289
+ 'precision': mode.split('_')[-1]
290
+ }
291
+ data_info = {
292
+ 'batch_size': FLAGS.batch_size,
293
+ 'shape': "dynamic_shape",
294
+ 'data_num': perf_info['img_num']
295
+ }
296
+ recognize_log = PaddleInferBenchmark(recognizer.config, model_info,
297
+ data_info, perf_info, mems)
298
+ recognize_log('Fight')
299
+
300
+
301
+ if __name__ == '__main__':
302
+ paddle.enable_static()
303
+ parser = argsparser()
304
+ FLAGS = parser.parse_args()
305
+ print_arguments(FLAGS)
306
+ FLAGS.device = FLAGS.device.upper()
307
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
308
+ ], "device should be CPU, GPU or XPU"
309
+
310
+ main()
pipeline/pphuman/video_action_preprocess.py ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from collections.abc import Sequence
18
+ from PIL import Image
19
+ import paddle
20
+
21
+
22
+ class Sampler(object):
23
+ """
24
+ Sample frames id.
25
+ NOTE: Use PIL to read image here, has diff with CV2
26
+ Args:
27
+ num_seg(int): number of segments.
28
+ seg_len(int): number of sampled frames in each segment.
29
+ valid_mode(bool): True or False.
30
+ Returns:
31
+ frames_idx: the index of sampled #frames.
32
+ """
33
+
34
+ def __init__(self,
35
+ num_seg,
36
+ seg_len,
37
+ frame_interval=None,
38
+ valid_mode=True,
39
+ dense_sample=False,
40
+ linspace_sample=False,
41
+ use_pil=True):
42
+ self.num_seg = num_seg
43
+ self.seg_len = seg_len
44
+ self.frame_interval = frame_interval
45
+ self.valid_mode = valid_mode
46
+ self.dense_sample = dense_sample
47
+ self.linspace_sample = linspace_sample
48
+ self.use_pil = use_pil
49
+
50
+ def _get(self, frames_idx, results):
51
+ data_format = results['format']
52
+
53
+ if data_format == "frame":
54
+ frame_dir = results['frame_dir']
55
+ imgs = []
56
+ for idx in frames_idx:
57
+ img = Image.open(
58
+ os.path.join(frame_dir, results['suffix'].format(
59
+ idx))).convert('RGB')
60
+ imgs.append(img)
61
+
62
+ elif data_format == "video":
63
+ if results['backend'] == 'cv2':
64
+ frames = np.array(results['frames'])
65
+ imgs = []
66
+ for idx in frames_idx:
67
+ imgbuf = frames[idx]
68
+ img = Image.fromarray(imgbuf, mode='RGB')
69
+ imgs.append(img)
70
+ elif results['backend'] == 'decord':
71
+ container = results['frames']
72
+ if self.use_pil:
73
+ frames_select = container.get_batch(frames_idx)
74
+ # dearray_to_img
75
+ np_frames = frames_select.asnumpy()
76
+ imgs = []
77
+ for i in range(np_frames.shape[0]):
78
+ imgbuf = np_frames[i]
79
+ imgs.append(Image.fromarray(imgbuf, mode='RGB'))
80
+ else:
81
+ if frames_idx.ndim != 1:
82
+ frames_idx = np.squeeze(frames_idx)
83
+ frame_dict = {
84
+ idx: container[idx].asnumpy()
85
+ for idx in np.unique(frames_idx)
86
+ }
87
+ imgs = [frame_dict[idx] for idx in frames_idx]
88
+ elif results['backend'] == 'pyav':
89
+ imgs = []
90
+ frames = np.array(results['frames'])
91
+ for idx in frames_idx:
92
+ imgbuf = frames[idx]
93
+ imgs.append(imgbuf)
94
+ imgs = np.stack(imgs) # thwc
95
+ else:
96
+ raise NotImplementedError
97
+ else:
98
+ raise NotImplementedError
99
+ results['imgs'] = imgs # all image data
100
+ return results
101
+
102
+ def _get_train_clips(self, num_frames):
103
+ ori_seg_len = self.seg_len * self.frame_interval
104
+ avg_interval = (num_frames - ori_seg_len + 1) // self.num_seg
105
+
106
+ if avg_interval > 0:
107
+ base_offsets = np.arange(self.num_seg) * avg_interval
108
+ clip_offsets = base_offsets + np.random.randint(
109
+ avg_interval, size=self.num_seg)
110
+ elif num_frames > max(self.num_seg, ori_seg_len):
111
+ clip_offsets = np.sort(
112
+ np.random.randint(
113
+ num_frames - ori_seg_len + 1, size=self.num_seg))
114
+ elif avg_interval == 0:
115
+ ratio = (num_frames - ori_seg_len + 1.0) / self.num_seg
116
+ clip_offsets = np.around(np.arange(self.num_seg) * ratio)
117
+ else:
118
+ clip_offsets = np.zeros((self.num_seg, ), dtype=np.int)
119
+ return clip_offsets
120
+
121
+ def _get_test_clips(self, num_frames):
122
+ ori_seg_len = self.seg_len * self.frame_interval
123
+ avg_interval = (num_frames - ori_seg_len + 1) / float(self.num_seg)
124
+ if num_frames > ori_seg_len - 1:
125
+ base_offsets = np.arange(self.num_seg) * avg_interval
126
+ clip_offsets = (base_offsets + avg_interval / 2.0).astype(np.int)
127
+ else:
128
+ clip_offsets = np.zeros((self.num_seg, ), dtype=np.int)
129
+ return clip_offsets
130
+
131
+ def __call__(self, results):
132
+ """
133
+ Args:
134
+ frames_len: length of frames.
135
+ return:
136
+ sampling id.
137
+ """
138
+ frames_len = int(results['frames_len']) # total number of frames
139
+
140
+ frames_idx = []
141
+ if self.frame_interval is not None:
142
+ assert isinstance(self.frame_interval, int)
143
+ if not self.valid_mode:
144
+ offsets = self._get_train_clips(frames_len)
145
+ else:
146
+ offsets = self._get_test_clips(frames_len)
147
+
148
+ offsets = offsets[:, None] + np.arange(self.seg_len)[
149
+ None, :] * self.frame_interval
150
+ offsets = np.concatenate(offsets)
151
+
152
+ offsets = offsets.reshape((-1, self.seg_len))
153
+ offsets = np.mod(offsets, frames_len)
154
+ offsets = np.concatenate(offsets)
155
+
156
+ if results['format'] == 'video':
157
+ frames_idx = offsets
158
+ elif results['format'] == 'frame':
159
+ frames_idx = list(offsets + 1)
160
+ else:
161
+ raise NotImplementedError
162
+
163
+ return self._get(frames_idx, results)
164
+
165
+ print("self.frame_interval:", self.frame_interval)
166
+
167
+ if self.linspace_sample: # default if False
168
+ if 'start_idx' in results and 'end_idx' in results:
169
+ offsets = np.linspace(results['start_idx'], results['end_idx'],
170
+ self.num_seg)
171
+ else:
172
+ offsets = np.linspace(0, frames_len - 1, self.num_seg)
173
+ offsets = np.clip(offsets, 0, frames_len - 1).astype(np.int64)
174
+ if results['format'] == 'video':
175
+ frames_idx = list(offsets)
176
+ frames_idx = [x % frames_len for x in frames_idx]
177
+ elif results['format'] == 'frame':
178
+ frames_idx = list(offsets + 1)
179
+ else:
180
+ raise NotImplementedError
181
+ return self._get(frames_idx, results)
182
+
183
+ average_dur = int(frames_len / self.num_seg)
184
+
185
+ print("results['format']:", results['format'])
186
+
187
+ if self.dense_sample: # For ppTSM, default is False
188
+ if not self.valid_mode: # train
189
+ sample_pos = max(1, 1 + frames_len - 64)
190
+ t_stride = 64 // self.num_seg
191
+ start_idx = 0 if sample_pos == 1 else np.random.randint(
192
+ 0, sample_pos - 1)
193
+ offsets = [(idx * t_stride + start_idx) % frames_len + 1
194
+ for idx in range(self.num_seg)]
195
+ frames_idx = offsets
196
+ else:
197
+ sample_pos = max(1, 1 + frames_len - 64)
198
+ t_stride = 64 // self.num_seg
199
+ start_list = np.linspace(0, sample_pos - 1, num=10, dtype=int)
200
+ offsets = []
201
+ for start_idx in start_list.tolist():
202
+ offsets += [(idx * t_stride + start_idx) % frames_len + 1
203
+ for idx in range(self.num_seg)]
204
+ frames_idx = offsets
205
+ else:
206
+ for i in range(self.num_seg):
207
+ idx = 0
208
+ if not self.valid_mode:
209
+ if average_dur >= self.seg_len:
210
+ idx = random.randint(0, average_dur - self.seg_len)
211
+ idx += i * average_dur
212
+ elif average_dur >= 1:
213
+ idx += i * average_dur
214
+ else:
215
+ idx = i
216
+ else:
217
+ if average_dur >= self.seg_len:
218
+ idx = (average_dur - 1) // 2
219
+ idx += i * average_dur
220
+ elif average_dur >= 1:
221
+ idx += i * average_dur
222
+ else:
223
+ idx = i
224
+
225
+ for jj in range(idx, idx + self.seg_len):
226
+ if results['format'] == 'video':
227
+ frames_idx.append(int(jj % frames_len))
228
+ elif results['format'] == 'frame':
229
+ frames_idx.append(jj + 1)
230
+
231
+ elif results['format'] == 'MRI':
232
+ frames_idx.append(jj)
233
+ else:
234
+ raise NotImplementedError
235
+
236
+ return self._get(frames_idx, results)
237
+
238
+
239
+ class Scale(object):
240
+ """
241
+ Scale images.
242
+ Args:
243
+ short_size(float | int): Short size of an image will be scaled to the short_size.
244
+ fixed_ratio(bool): Set whether to zoom according to a fixed ratio. default: True
245
+ do_round(bool): Whether to round up when calculating the zoom ratio. default: False
246
+ backend(str): Choose pillow or cv2 as the graphics processing backend. default: 'pillow'
247
+ """
248
+
249
+ def __init__(self,
250
+ short_size,
251
+ fixed_ratio=True,
252
+ keep_ratio=None,
253
+ do_round=False,
254
+ backend='pillow'):
255
+ self.short_size = short_size
256
+ assert (fixed_ratio and not keep_ratio) or (
257
+ not fixed_ratio
258
+ ), "fixed_ratio and keep_ratio cannot be true at the same time"
259
+ self.fixed_ratio = fixed_ratio
260
+ self.keep_ratio = keep_ratio
261
+ self.do_round = do_round
262
+
263
+ assert backend in [
264
+ 'pillow', 'cv2'
265
+ ], "Scale's backend must be pillow or cv2, but get {backend}"
266
+
267
+ self.backend = backend
268
+
269
+ def __call__(self, results):
270
+ """
271
+ Performs resize operations.
272
+ Args:
273
+ imgs (Sequence[PIL.Image]): List where each item is a PIL.Image.
274
+ For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...]
275
+ return:
276
+ resized_imgs: List where each item is a PIL.Image after scaling.
277
+ """
278
+ imgs = results['imgs']
279
+ resized_imgs = []
280
+ for i in range(len(imgs)):
281
+ img = imgs[i]
282
+ if isinstance(img, np.ndarray):
283
+ h, w, _ = img.shape
284
+ elif isinstance(img, Image.Image):
285
+ w, h = img.size
286
+ else:
287
+ raise NotImplementedError
288
+
289
+ if w <= h:
290
+ ow = self.short_size
291
+ if self.fixed_ratio: # default is True
292
+ oh = int(self.short_size * 4.0 / 3.0)
293
+ elif not self.keep_ratio: # no
294
+ oh = self.short_size
295
+ else:
296
+ scale_factor = self.short_size / w
297
+ oh = int(h * float(scale_factor) +
298
+ 0.5) if self.do_round else int(
299
+ h * self.short_size / w)
300
+ ow = int(w * float(scale_factor) +
301
+ 0.5) if self.do_round else int(
302
+ w * self.short_size / h)
303
+ else:
304
+ oh = self.short_size
305
+ if self.fixed_ratio:
306
+ ow = int(self.short_size * 4.0 / 3.0)
307
+ elif not self.keep_ratio: # no
308
+ ow = self.short_size
309
+ else:
310
+ scale_factor = self.short_size / h
311
+ oh = int(h * float(scale_factor) +
312
+ 0.5) if self.do_round else int(
313
+ h * self.short_size / w)
314
+ ow = int(w * float(scale_factor) +
315
+ 0.5) if self.do_round else int(
316
+ w * self.short_size / h)
317
+
318
+ if type(img) == np.ndarray:
319
+ img = Image.fromarray(img, mode='RGB')
320
+
321
+ if self.backend == 'pillow':
322
+ resized_imgs.append(img.resize((ow, oh), Image.BILINEAR))
323
+ elif self.backend == 'cv2' and (self.keep_ratio is not None):
324
+ resized_imgs.append(
325
+ cv2.resize(
326
+ img, (ow, oh), interpolation=cv2.INTER_LINEAR))
327
+ else:
328
+ resized_imgs.append(
329
+ Image.fromarray(
330
+ cv2.resize(
331
+ np.asarray(img), (ow, oh),
332
+ interpolation=cv2.INTER_LINEAR)))
333
+ results['imgs'] = resized_imgs
334
+ return results
335
+
336
+
337
+ class CenterCrop(object):
338
+ """
339
+ Center crop images
340
+ Args:
341
+ target_size(int): Center crop a square with the target_size from an image.
342
+ do_round(bool): Whether to round up the coordinates of the upper left corner of the cropping area. default: True
343
+ """
344
+
345
+ def __init__(self, target_size, do_round=True, backend='pillow'):
346
+ self.target_size = target_size
347
+ self.do_round = do_round
348
+ self.backend = backend
349
+
350
+ def __call__(self, results):
351
+ """
352
+ Performs Center crop operations.
353
+ Args:
354
+ imgs: List where each item is a PIL.Image.
355
+ For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...]
356
+ return:
357
+ ccrop_imgs: List where each item is a PIL.Image after Center crop.
358
+ """
359
+ imgs = results['imgs']
360
+ ccrop_imgs = []
361
+ th, tw = self.target_size, self.target_size
362
+ if isinstance(imgs, paddle.Tensor):
363
+ h, w = imgs.shape[-2:]
364
+ x1 = int(round((w - tw) / 2.0)) if self.do_round else (w - tw) // 2
365
+ y1 = int(round((h - th) / 2.0)) if self.do_round else (h - th) // 2
366
+ ccrop_imgs = imgs[:, :, y1:y1 + th, x1:x1 + tw]
367
+ else:
368
+ for img in imgs:
369
+ if self.backend == 'pillow':
370
+ w, h = img.size
371
+ elif self.backend == 'cv2':
372
+ h, w, _ = img.shape
373
+ else:
374
+ raise NotImplementedError
375
+ assert (w >= self.target_size) and (h >= self.target_size), \
376
+ "image width({}) and height({}) should be larger than crop size".format(
377
+ w, h, self.target_size)
378
+ x1 = int(round((w - tw) / 2.0)) if self.do_round else (
379
+ w - tw) // 2
380
+ y1 = int(round((h - th) / 2.0)) if self.do_round else (
381
+ h - th) // 2
382
+ if self.backend == 'cv2':
383
+ ccrop_imgs.append(img[y1:y1 + th, x1:x1 + tw])
384
+ elif self.backend == 'pillow':
385
+ ccrop_imgs.append(img.crop((x1, y1, x1 + tw, y1 + th)))
386
+ results['imgs'] = ccrop_imgs
387
+ return results
388
+
389
+
390
+ class Image2Array(object):
391
+ """
392
+ transfer PIL.Image to Numpy array and transpose dimensions from 'dhwc' to 'dchw'.
393
+ Args:
394
+ transpose: whether to transpose or not, default True, False for slowfast.
395
+ """
396
+
397
+ def __init__(self, transpose=True, data_format='tchw'):
398
+ assert data_format in [
399
+ 'tchw', 'cthw'
400
+ ], "Target format must in ['tchw', 'cthw'], but got {data_format}"
401
+ self.transpose = transpose
402
+ self.data_format = data_format
403
+
404
+ def __call__(self, results):
405
+ """
406
+ Performs Image to NumpyArray operations.
407
+ Args:
408
+ imgs: List where each item is a PIL.Image.
409
+ For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...]
410
+ return:
411
+ np_imgs: Numpy array.
412
+ """
413
+ imgs = results['imgs']
414
+ if 'backend' in results and results[
415
+ 'backend'] == 'pyav': # [T,H,W,C] in [0, 1]
416
+ if self.transpose:
417
+ if self.data_format == 'tchw':
418
+ t_imgs = imgs.transpose((0, 3, 1, 2)) # tchw
419
+ else:
420
+ t_imgs = imgs.transpose((3, 0, 1, 2)) # cthw
421
+ results['imgs'] = t_imgs
422
+ else:
423
+ t_imgs = np.stack(imgs).astype('float32')
424
+ if self.transpose:
425
+ if self.data_format == 'tchw':
426
+ t_imgs = t_imgs.transpose(0, 3, 1, 2) # tchw
427
+ else:
428
+ t_imgs = t_imgs.transpose(3, 0, 1, 2) # cthw
429
+ results['imgs'] = t_imgs
430
+ return results
431
+
432
+
433
+ class VideoDecoder(object):
434
+ """
435
+ Decode mp4 file to frames.
436
+ Args:
437
+ filepath: the file path of mp4 file
438
+ """
439
+
440
+ def __init__(self,
441
+ backend='cv2',
442
+ mode='train',
443
+ sampling_rate=32,
444
+ num_seg=8,
445
+ num_clips=1,
446
+ target_fps=30):
447
+
448
+ self.backend = backend
449
+ # params below only for TimeSformer
450
+ self.mode = mode
451
+ self.sampling_rate = sampling_rate
452
+ self.num_seg = num_seg
453
+ self.num_clips = num_clips
454
+ self.target_fps = target_fps
455
+
456
+ def __call__(self, results):
457
+ """
458
+ Perform mp4 decode operations.
459
+ return:
460
+ List where each item is a numpy array after decoder.
461
+ """
462
+ file_path = results['filename']
463
+ results['format'] = 'video'
464
+ results['backend'] = self.backend
465
+
466
+ if self.backend == 'cv2': # here
467
+ cap = cv2.VideoCapture(file_path)
468
+ videolen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
469
+
470
+ sampledFrames = []
471
+ for i in range(videolen):
472
+ ret, frame = cap.read()
473
+ # maybe first frame is empty
474
+ if ret == False:
475
+ continue
476
+ img = frame[:, :, ::-1]
477
+ sampledFrames.append(img)
478
+ results['frames'] = sampledFrames
479
+ results['frames_len'] = len(sampledFrames)
480
+
481
+ elif self.backend == 'decord':
482
+ container = de.VideoReader(file_path)
483
+ frames_len = len(container)
484
+ results['frames'] = container
485
+ results['frames_len'] = frames_len
486
+ else:
487
+ raise NotImplementedError
488
+ return results
489
+
490
+
491
+ class Normalization(object):
492
+ """
493
+ Normalization.
494
+ Args:
495
+ mean(Sequence[float]): mean values of different channels.
496
+ std(Sequence[float]): std values of different channels.
497
+ tensor_shape(list): size of mean, default [3,1,1]. For slowfast, [1,1,1,3]
498
+ """
499
+
500
+ def __init__(self, mean, std, tensor_shape=[3, 1, 1], inplace=False):
501
+ if not isinstance(mean, Sequence):
502
+ raise TypeError(
503
+ 'Mean must be list, tuple or np.ndarray, but got {type(mean)}')
504
+ if not isinstance(std, Sequence):
505
+ raise TypeError(
506
+ 'Std must be list, tuple or np.ndarray, but got {type(std)}')
507
+
508
+ self.inplace = inplace
509
+ if not inplace:
510
+ self.mean = np.array(mean).reshape(tensor_shape).astype(np.float32)
511
+ self.std = np.array(std).reshape(tensor_shape).astype(np.float32)
512
+ else:
513
+ self.mean = np.array(mean, dtype=np.float32)
514
+ self.std = np.array(std, dtype=np.float32)
515
+
516
+ def __call__(self, results):
517
+ """
518
+ Performs normalization operations.
519
+ Args:
520
+ imgs: Numpy array.
521
+ return:
522
+ np_imgs: Numpy array after normalization.
523
+ """
524
+
525
+ if self.inplace: # default is False
526
+ n = len(results['imgs'])
527
+ h, w, c = results['imgs'][0].shape
528
+ norm_imgs = np.empty((n, h, w, c), dtype=np.float32)
529
+ for i, img in enumerate(results['imgs']):
530
+ norm_imgs[i] = img
531
+
532
+ for img in norm_imgs: # [n,h,w,c]
533
+ mean = np.float64(self.mean.reshape(1, -1)) # [1, 3]
534
+ stdinv = 1 / np.float64(self.std.reshape(1, -1)) # [1, 3]
535
+ cv2.subtract(img, mean, img)
536
+ cv2.multiply(img, stdinv, img)
537
+ else:
538
+ imgs = results['imgs']
539
+ norm_imgs = imgs / 255.0
540
+ norm_imgs -= self.mean
541
+ norm_imgs /= self.std
542
+ if 'backend' in results and results['backend'] == 'pyav':
543
+ norm_imgs = paddle.to_tensor(norm_imgs, dtype=paddle.float32)
544
+ results['imgs'] = norm_imgs
545
+ return results
pptracking/python/det_infer.py ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+ from functools import reduce
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import math
23
+
24
+ import paddle
25
+ from paddle.inference import Config
26
+ from paddle.inference import create_predictor
27
+
28
+ import sys
29
+ # add deploy path of PadleDetection to sys.path
30
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'])))
31
+ sys.path.insert(0, parent_path)
32
+
33
+ from benchmark_utils import PaddleInferBenchmark
34
+ from picodet_postprocess import PicoDetPostProcess
35
+ from preprocess import preprocess, Resize, NormalizeImage, Permute, PadStride, LetterBoxResize, Pad, decode_image
36
+ from mot.visualize import visualize_box_mask
37
+ from mot_utils import argsparser, Timer, get_current_memory_mb
38
+
39
+ # Global dictionary
40
+ SUPPORT_MODELS = {
41
+ 'YOLO',
42
+ 'PicoDet',
43
+ 'JDE',
44
+ 'FairMOT',
45
+ 'DeepSORT',
46
+ 'StrongBaseline',
47
+ }
48
+
49
+
50
+ def bench_log(detector, img_list, model_info, batch_size=1, name=None):
51
+ mems = {
52
+ 'cpu_rss_mb': detector.cpu_mem / len(img_list),
53
+ 'gpu_rss_mb': detector.gpu_mem / len(img_list),
54
+ 'gpu_util': detector.gpu_util * 100 / len(img_list)
55
+ }
56
+ perf_info = detector.det_times.report(average=True)
57
+ data_info = {
58
+ 'batch_size': batch_size,
59
+ 'shape': "dynamic_shape",
60
+ 'data_num': perf_info['img_num']
61
+ }
62
+ log = PaddleInferBenchmark(detector.config, model_info, data_info,
63
+ perf_info, mems)
64
+ log(name)
65
+
66
+
67
+ class Detector(object):
68
+ """
69
+ Args:
70
+ pred_config (object): config of model, defined by `Config(model_dir)`
71
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
72
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
73
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
74
+ batch_size (int): size of pre batch in inference
75
+ trt_min_shape (int): min shape for dynamic shape in trt
76
+ trt_max_shape (int): max shape for dynamic shape in trt
77
+ trt_opt_shape (int): opt shape for dynamic shape in trt
78
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
79
+ calibration, trt_calib_mode need to set True
80
+ cpu_threads (int): cpu threads
81
+ enable_mkldnn (bool): whether to open MKLDNN
82
+ output_dir (str): The path of output
83
+ threshold (float): The threshold of score for visualization
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ model_dir,
89
+ device='CPU',
90
+ run_mode='paddle',
91
+ batch_size=1,
92
+ trt_min_shape=1,
93
+ trt_max_shape=1280,
94
+ trt_opt_shape=640,
95
+ trt_calib_mode=False,
96
+ cpu_threads=1,
97
+ enable_mkldnn=False,
98
+ output_dir='output',
99
+ threshold=0.5, ):
100
+ self.pred_config = self.set_config(model_dir)
101
+ self.predictor, self.config = load_predictor(
102
+ model_dir,
103
+ run_mode=run_mode,
104
+ batch_size=batch_size,
105
+ min_subgraph_size=self.pred_config.min_subgraph_size,
106
+ device=device,
107
+ use_dynamic_shape=self.pred_config.use_dynamic_shape,
108
+ trt_min_shape=trt_min_shape,
109
+ trt_max_shape=trt_max_shape,
110
+ trt_opt_shape=trt_opt_shape,
111
+ trt_calib_mode=trt_calib_mode,
112
+ cpu_threads=cpu_threads,
113
+ enable_mkldnn=enable_mkldnn)
114
+ self.det_times = Timer()
115
+ self.cpu_mem, self.gpu_mem, self.gpu_util = 0, 0, 0
116
+ self.batch_size = batch_size
117
+ self.output_dir = output_dir
118
+ self.threshold = threshold
119
+
120
+ def set_config(self, model_dir):
121
+ return PredictConfig(model_dir)
122
+
123
+ def preprocess(self, image_list):
124
+ preprocess_ops = []
125
+ for op_info in self.pred_config.preprocess_infos:
126
+ new_op_info = op_info.copy()
127
+ op_type = new_op_info.pop('type')
128
+ preprocess_ops.append(eval(op_type)(**new_op_info))
129
+
130
+ input_im_lst = []
131
+ input_im_info_lst = []
132
+ for im_path in image_list:
133
+ im, im_info = preprocess(im_path, preprocess_ops)
134
+ input_im_lst.append(im)
135
+ input_im_info_lst.append(im_info)
136
+ inputs = create_inputs(input_im_lst, input_im_info_lst)
137
+ input_names = self.predictor.get_input_names()
138
+ for i in range(len(input_names)):
139
+ input_tensor = self.predictor.get_input_handle(input_names[i])
140
+ input_tensor.copy_from_cpu(inputs[input_names[i]])
141
+
142
+ return inputs
143
+
144
+ def postprocess(self, inputs, result):
145
+ # postprocess output of predictor
146
+ np_boxes_num = result['boxes_num']
147
+ if np_boxes_num[0] <= 0:
148
+ print('[WARNNING] No object detected.')
149
+ result = {'boxes': np.zeros([0, 6]), 'boxes_num': [0]}
150
+ result = {k: v for k, v in result.items() if v is not None}
151
+ return result
152
+
153
+ def predict(self, repeats=1):
154
+ '''
155
+ Args:
156
+ repeats (int): repeats number for prediction
157
+ Returns:
158
+ result (dict): include 'boxes': np.ndarray: shape:[N,6], N: number of box,
159
+ matix element:[class, score, x_min, y_min, x_max, y_max]
160
+ '''
161
+ # model prediction
162
+ np_boxes, np_boxes_num = None, None
163
+ for i in range(repeats):
164
+ self.predictor.run()
165
+ output_names = self.predictor.get_output_names()
166
+ boxes_tensor = self.predictor.get_output_handle(output_names[0])
167
+ np_boxes = boxes_tensor.copy_to_cpu()
168
+ boxes_num = self.predictor.get_output_handle(output_names[1])
169
+ np_boxes_num = boxes_num.copy_to_cpu()
170
+ result = dict(boxes=np_boxes, boxes_num=np_boxes_num)
171
+ return result
172
+
173
+ def merge_batch_result(self, batch_result):
174
+ if len(batch_result) == 1:
175
+ return batch_result[0]
176
+ res_key = batch_result[0].keys()
177
+ results = {k: [] for k in res_key}
178
+ for res in batch_result:
179
+ for k, v in res.items():
180
+ results[k].append(v)
181
+ for k, v in results.items():
182
+ results[k] = np.concatenate(v)
183
+ return results
184
+
185
+ def get_timer(self):
186
+ return self.det_times
187
+
188
+ def predict_image(self,
189
+ image_list,
190
+ run_benchmark=False,
191
+ repeats=1,
192
+ visual=True):
193
+ batch_loop_cnt = math.ceil(float(len(image_list)) / self.batch_size)
194
+ results = []
195
+ for i in range(batch_loop_cnt):
196
+ start_index = i * self.batch_size
197
+ end_index = min((i + 1) * self.batch_size, len(image_list))
198
+ batch_image_list = image_list[start_index:end_index]
199
+ if run_benchmark:
200
+ # preprocess
201
+ inputs = self.preprocess(batch_image_list) # warmup
202
+ self.det_times.preprocess_time_s.start()
203
+ inputs = self.preprocess(batch_image_list)
204
+ self.det_times.preprocess_time_s.end()
205
+
206
+ # model prediction
207
+ result = self.predict(repeats=repeats) # warmup
208
+ self.det_times.inference_time_s.start()
209
+ result = self.predict(repeats=repeats)
210
+ self.det_times.inference_time_s.end(repeats=repeats)
211
+
212
+ # postprocess
213
+ result_warmup = self.postprocess(inputs, result) # warmup
214
+ self.det_times.postprocess_time_s.start()
215
+ result = self.postprocess(inputs, result)
216
+ self.det_times.postprocess_time_s.end()
217
+ self.det_times.img_num += len(batch_image_list)
218
+
219
+ cm, gm, gu = get_current_memory_mb()
220
+ self.cpu_mem += cm
221
+ self.gpu_mem += gm
222
+ self.gpu_util += gu
223
+ else:
224
+ # preprocess
225
+ self.det_times.preprocess_time_s.start()
226
+ inputs = self.preprocess(batch_image_list)
227
+ self.det_times.preprocess_time_s.end()
228
+
229
+ # model prediction
230
+ self.det_times.inference_time_s.start()
231
+ result = self.predict()
232
+ self.det_times.inference_time_s.end()
233
+
234
+ # postprocess
235
+ self.det_times.postprocess_time_s.start()
236
+ result = self.postprocess(inputs, result)
237
+ self.det_times.postprocess_time_s.end()
238
+ self.det_times.img_num += len(batch_image_list)
239
+
240
+ if visual:
241
+ visualize(
242
+ batch_image_list,
243
+ result,
244
+ self.pred_config.labels,
245
+ output_dir=self.output_dir,
246
+ threshold=self.threshold)
247
+
248
+ results.append(result)
249
+ if visual:
250
+ print('Test iter {}'.format(i))
251
+
252
+ results = self.merge_batch_result(results)
253
+ return results
254
+
255
+ def predict_video(self, video_file, camera_id):
256
+ video_out_name = 'output.mp4'
257
+ if camera_id != -1:
258
+ capture = cv2.VideoCapture(camera_id)
259
+ else:
260
+ capture = cv2.VideoCapture(video_file)
261
+ video_out_name = os.path.split(video_file)[-1]
262
+ # Get Video info : resolution, fps, frame count
263
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
264
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
265
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
266
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
267
+ print("fps: %d, frame_count: %d" % (fps, frame_count))
268
+
269
+ if not os.path.exists(self.output_dir):
270
+ os.makedirs(self.output_dir)
271
+ out_path = os.path.join(self.output_dir, video_out_name)
272
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
273
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
274
+ index = 1
275
+ while (1):
276
+ ret, frame = capture.read()
277
+ if not ret:
278
+ break
279
+ print('detect frame: %d' % (index))
280
+ index += 1
281
+ results = self.predict_image([frame], visual=False)
282
+
283
+ im = visualize_box_mask(
284
+ frame,
285
+ results,
286
+ self.pred_config.labels,
287
+ threshold=self.threshold)
288
+ im = np.array(im)
289
+ writer.write(im)
290
+ if camera_id != -1:
291
+ cv2.imshow('Mask Detection', im)
292
+ if cv2.waitKey(1) & 0xFF == ord('q'):
293
+ break
294
+ writer.release()
295
+
296
+
297
+ def create_inputs(imgs, im_info):
298
+ """generate input for different model type
299
+ Args:
300
+ imgs (list(numpy)): list of images (np.ndarray)
301
+ im_info (list(dict)): list of image info
302
+ Returns:
303
+ inputs (dict): input of model
304
+ """
305
+ inputs = {}
306
+
307
+ im_shape = []
308
+ scale_factor = []
309
+ if len(imgs) == 1:
310
+ inputs['image'] = np.array((imgs[0], )).astype('float32')
311
+ inputs['im_shape'] = np.array(
312
+ (im_info[0]['im_shape'], )).astype('float32')
313
+ inputs['scale_factor'] = np.array(
314
+ (im_info[0]['scale_factor'], )).astype('float32')
315
+ return inputs
316
+
317
+ for e in im_info:
318
+ im_shape.append(np.array((e['im_shape'], )).astype('float32'))
319
+ scale_factor.append(np.array((e['scale_factor'], )).astype('float32'))
320
+
321
+ inputs['im_shape'] = np.concatenate(im_shape, axis=0)
322
+ inputs['scale_factor'] = np.concatenate(scale_factor, axis=0)
323
+
324
+ imgs_shape = [[e.shape[1], e.shape[2]] for e in imgs]
325
+ max_shape_h = max([e[0] for e in imgs_shape])
326
+ max_shape_w = max([e[1] for e in imgs_shape])
327
+ padding_imgs = []
328
+ for img in imgs:
329
+ im_c, im_h, im_w = img.shape[:]
330
+ padding_im = np.zeros(
331
+ (im_c, max_shape_h, max_shape_w), dtype=np.float32)
332
+ padding_im[:, :im_h, :im_w] = img
333
+ padding_imgs.append(padding_im)
334
+ inputs['image'] = np.stack(padding_imgs, axis=0)
335
+ return inputs
336
+
337
+
338
+ class PredictConfig():
339
+ """set config of preprocess, postprocess and visualize
340
+ Args:
341
+ model_dir (str): root path of model.yml
342
+ """
343
+
344
+ def __init__(self, model_dir):
345
+ # parsing Yaml config for Preprocess
346
+ deploy_file = os.path.join(model_dir, 'infer_cfg.yml')
347
+ with open(deploy_file) as f:
348
+ yml_conf = yaml.safe_load(f)
349
+ self.check_model(yml_conf)
350
+ self.arch = yml_conf['arch']
351
+ self.preprocess_infos = yml_conf['Preprocess']
352
+ self.min_subgraph_size = yml_conf['min_subgraph_size']
353
+ self.labels = yml_conf['label_list']
354
+ self.mask = False
355
+ self.use_dynamic_shape = yml_conf['use_dynamic_shape']
356
+ if 'mask' in yml_conf:
357
+ self.mask = yml_conf['mask']
358
+ self.tracker = None
359
+ if 'tracker' in yml_conf:
360
+ self.tracker = yml_conf['tracker']
361
+ if 'NMS' in yml_conf:
362
+ self.nms = yml_conf['NMS']
363
+ if 'fpn_stride' in yml_conf:
364
+ self.fpn_stride = yml_conf['fpn_stride']
365
+ self.print_config()
366
+
367
+ def check_model(self, yml_conf):
368
+ """
369
+ Raises:
370
+ ValueError: loaded model not in supported model type
371
+ """
372
+ for support_model in SUPPORT_MODELS:
373
+ if support_model in yml_conf['arch']:
374
+ return True
375
+ raise ValueError("Unsupported arch: {}, expect {}".format(yml_conf[
376
+ 'arch'], SUPPORT_MODELS))
377
+
378
+ def print_config(self):
379
+ print('----------- Model Configuration -----------')
380
+ print('%s: %s' % ('Model Arch', self.arch))
381
+ print('%s: ' % ('Transform Order'))
382
+ for op_info in self.preprocess_infos:
383
+ print('--%s: %s' % ('transform op', op_info['type']))
384
+ print('--------------------------------------------')
385
+
386
+
387
+ def load_predictor(model_dir,
388
+ run_mode='paddle',
389
+ batch_size=1,
390
+ device='CPU',
391
+ min_subgraph_size=3,
392
+ use_dynamic_shape=False,
393
+ trt_min_shape=1,
394
+ trt_max_shape=1280,
395
+ trt_opt_shape=640,
396
+ trt_calib_mode=False,
397
+ cpu_threads=1,
398
+ enable_mkldnn=False):
399
+ """set AnalysisConfig, generate AnalysisPredictor
400
+ Args:
401
+ model_dir (str): root path of __model__ and __params__
402
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
403
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16/trt_int8)
404
+ use_dynamic_shape (bool): use dynamic shape or not
405
+ trt_min_shape (int): min shape for dynamic shape in trt
406
+ trt_max_shape (int): max shape for dynamic shape in trt
407
+ trt_opt_shape (int): opt shape for dynamic shape in trt
408
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
409
+ calibration, trt_calib_mode need to set True
410
+ Returns:
411
+ predictor (PaddlePredictor): AnalysisPredictor
412
+ Raises:
413
+ ValueError: predict by TensorRT need device == 'GPU'.
414
+ """
415
+ if device != 'GPU' and run_mode != 'paddle':
416
+ raise ValueError(
417
+ "Predict by TensorRT mode: {}, expect device=='GPU', but device == {}"
418
+ .format(run_mode, device))
419
+ infer_model = os.path.join(model_dir, 'model.pdmodel')
420
+ infer_params = os.path.join(model_dir, 'model.pdiparams')
421
+ if not os.path.exists(infer_model):
422
+ infer_model = os.path.join(model_dir, 'inference.pdmodel')
423
+ infer_params = os.path.join(model_dir, 'inference.pdiparams')
424
+ if not os.path.exists(infer_model):
425
+ raise ValueError("Cannot find any inference model in dir: {},".
426
+ format(model_dir))
427
+ config = Config(infer_model, infer_params)
428
+ if device == 'GPU':
429
+ # initial GPU memory(M), device ID
430
+ config.enable_use_gpu(200, 0)
431
+ # optimize graph and fuse op
432
+ config.switch_ir_optim(True)
433
+ elif device == 'XPU':
434
+ config.enable_lite_engine()
435
+ config.enable_xpu(10 * 1024 * 1024)
436
+ else:
437
+ config.disable_gpu()
438
+ config.set_cpu_math_library_num_threads(cpu_threads)
439
+ if enable_mkldnn:
440
+ try:
441
+ # cache 10 different shapes for mkldnn to avoid memory leak
442
+ config.set_mkldnn_cache_capacity(10)
443
+ config.enable_mkldnn()
444
+ except Exception as e:
445
+ print(
446
+ "The current environment does not support `mkldnn`, so disable mkldnn."
447
+ )
448
+ pass
449
+
450
+ precision_map = {
451
+ 'trt_int8': Config.Precision.Int8,
452
+ 'trt_fp32': Config.Precision.Float32,
453
+ 'trt_fp16': Config.Precision.Half
454
+ }
455
+ if run_mode in precision_map.keys():
456
+ config.enable_tensorrt_engine(
457
+ workspace_size=1 << 25,
458
+ max_batch_size=batch_size,
459
+ min_subgraph_size=min_subgraph_size,
460
+ precision_mode=precision_map[run_mode],
461
+ use_static=False,
462
+ use_calib_mode=trt_calib_mode)
463
+
464
+ if use_dynamic_shape:
465
+ min_input_shape = {
466
+ 'image': [batch_size, 3, trt_min_shape, trt_min_shape]
467
+ }
468
+ max_input_shape = {
469
+ 'image': [batch_size, 3, trt_max_shape, trt_max_shape]
470
+ }
471
+ opt_input_shape = {
472
+ 'image': [batch_size, 3, trt_opt_shape, trt_opt_shape]
473
+ }
474
+ config.set_trt_dynamic_shape_info(min_input_shape, max_input_shape,
475
+ opt_input_shape)
476
+ print('trt set dynamic shape done!')
477
+
478
+ # disable print log when predict
479
+ config.disable_glog_info()
480
+ # enable shared memory
481
+ config.enable_memory_optim()
482
+ # disable feed, fetch OP, needed by zero_copy_run
483
+ config.switch_use_feed_fetch_ops(False)
484
+ predictor = create_predictor(config)
485
+ return predictor, config
486
+
487
+
488
+ def get_test_images(infer_dir, infer_img):
489
+ """
490
+ Get image path list in TEST mode
491
+ """
492
+ assert infer_img is not None or infer_dir is not None, \
493
+ "--infer_img or --infer_dir should be set"
494
+ assert infer_img is None or os.path.isfile(infer_img), \
495
+ "{} is not a file".format(infer_img)
496
+ assert infer_dir is None or os.path.isdir(infer_dir), \
497
+ "{} is not a directory".format(infer_dir)
498
+
499
+ # infer_img has a higher priority
500
+ if infer_img and os.path.isfile(infer_img):
501
+ return [infer_img]
502
+
503
+ images = set()
504
+ infer_dir = os.path.abspath(infer_dir)
505
+ assert os.path.isdir(infer_dir), \
506
+ "infer_dir {} is not a directory".format(infer_dir)
507
+ exts = ['jpg', 'jpeg', 'png', 'bmp']
508
+ exts += [ext.upper() for ext in exts]
509
+ for ext in exts:
510
+ images.update(glob.glob('{}/*.{}'.format(infer_dir, ext)))
511
+ images = list(images)
512
+
513
+ assert len(images) > 0, "no image found in {}".format(infer_dir)
514
+ print("Found {} inference images in total.".format(len(images)))
515
+
516
+ return images
517
+
518
+
519
+ def visualize(image_list, result, labels, output_dir='output/', threshold=0.5):
520
+ # visualize the predict result
521
+ start_idx = 0
522
+ for idx, image_file in enumerate(image_list):
523
+ im_bboxes_num = result['boxes_num'][idx]
524
+ im_results = {}
525
+ if 'boxes' in result:
526
+ im_results['boxes'] = result['boxes'][start_idx:start_idx +
527
+ im_bboxes_num, :]
528
+ start_idx += im_bboxes_num
529
+ im = visualize_box_mask(
530
+ image_file, im_results, labels, threshold=threshold)
531
+ img_name = os.path.split(image_file)[-1]
532
+ if not os.path.exists(output_dir):
533
+ os.makedirs(output_dir)
534
+ out_path = os.path.join(output_dir, img_name)
535
+ im.save(out_path, quality=95)
536
+ print("save result to: " + out_path)
537
+
538
+
539
+ def print_arguments(args):
540
+ print('----------- Running Arguments -----------')
541
+ for arg, value in sorted(vars(args).items()):
542
+ print('%s: %s' % (arg, value))
543
+ print('------------------------------------------')
544
+
545
+
546
+ def main():
547
+ deploy_file = os.path.join(FLAGS.model_dir, 'infer_cfg.yml')
548
+ with open(deploy_file) as f:
549
+ yml_conf = yaml.safe_load(f)
550
+ arch = yml_conf['arch']
551
+ detector_func = 'Detector'
552
+ detector = eval(detector_func)(FLAGS.model_dir,
553
+ device=FLAGS.device,
554
+ run_mode=FLAGS.run_mode,
555
+ batch_size=FLAGS.batch_size,
556
+ trt_min_shape=FLAGS.trt_min_shape,
557
+ trt_max_shape=FLAGS.trt_max_shape,
558
+ trt_opt_shape=FLAGS.trt_opt_shape,
559
+ trt_calib_mode=FLAGS.trt_calib_mode,
560
+ cpu_threads=FLAGS.cpu_threads,
561
+ enable_mkldnn=FLAGS.enable_mkldnn,
562
+ threshold=FLAGS.threshold,
563
+ output_dir=FLAGS.output_dir)
564
+
565
+ # predict from video file or camera video stream
566
+ if FLAGS.video_file is not None or FLAGS.camera_id != -1:
567
+ detector.predict_video(FLAGS.video_file, FLAGS.camera_id)
568
+ else:
569
+ # predict from image
570
+ if FLAGS.image_dir is None and FLAGS.image_file is not None:
571
+ assert FLAGS.batch_size == 1, "batch_size should be 1, when image_file is not None"
572
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
573
+ detector.predict_image(img_list, FLAGS.run_benchmark, repeats=10)
574
+ if not FLAGS.run_benchmark:
575
+ detector.det_times.info(average=True)
576
+ else:
577
+ mode = FLAGS.run_mode
578
+ model_dir = FLAGS.model_dir
579
+ model_info = {
580
+ 'model_name': model_dir.strip('/').split('/')[-1],
581
+ 'precision': mode.split('_')[-1]
582
+ }
583
+ bench_log(detector, img_list, model_info, name='DET')
584
+
585
+
586
+ if __name__ == '__main__':
587
+ paddle.enable_static()
588
+ parser = argsparser()
589
+ FLAGS = parser.parse_args()
590
+ print_arguments(FLAGS)
591
+ FLAGS.device = FLAGS.device.upper()
592
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
593
+ ], "device should be CPU, GPU or XPU"
594
+ main()
pptracking/python/mot/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import matching
16
+ from . import tracker
17
+ from . import motion
18
+ from . import utils
19
+ from . import mtmct
20
+
21
+ from .matching import *
22
+ from .tracker import *
23
+ from .motion import *
24
+ from .utils import *
25
+ from .mtmct import *
pptracking/python/mot/matching/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import jde_matching
16
+ from . import deepsort_matching
17
+ from . import ocsort_matching
18
+
19
+ from .jde_matching import *
20
+ from .deepsort_matching import *
21
+ from .ocsort_matching import *
pptracking/python/mot/matching/deepsort_matching.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/nwojke/deep_sort/tree/master/deep_sort
16
+ """
17
+
18
+ import numpy as np
19
+ from scipy.optimize import linear_sum_assignment
20
+ from ..motion import kalman_filter
21
+
22
+ INFTY_COST = 1e+5
23
+
24
+ __all__ = [
25
+ 'iou_1toN',
26
+ 'iou_cost',
27
+ '_nn_euclidean_distance',
28
+ '_nn_cosine_distance',
29
+ 'NearestNeighborDistanceMetric',
30
+ 'min_cost_matching',
31
+ 'matching_cascade',
32
+ 'gate_cost_matrix',
33
+ ]
34
+
35
+
36
+ def iou_1toN(bbox, candidates):
37
+ """
38
+ Computer intersection over union (IoU) by one box to N candidates.
39
+
40
+ Args:
41
+ bbox (ndarray): A bounding box in format `(top left x, top left y, width, height)`.
42
+ candidates (ndarray): A matrix of candidate bounding boxes (one per row) in the
43
+ same format as `bbox`.
44
+
45
+ Returns:
46
+ ious (ndarray): The intersection over union in [0, 1] between the `bbox`
47
+ and each candidate. A higher score means a larger fraction of the
48
+ `bbox` is occluded by the candidate.
49
+ """
50
+ bbox_tl = bbox[:2]
51
+ bbox_br = bbox[:2] + bbox[2:]
52
+ candidates_tl = candidates[:, :2]
53
+ candidates_br = candidates[:, :2] + candidates[:, 2:]
54
+
55
+ tl = np.c_[np.maximum(bbox_tl[0], candidates_tl[:, 0])[:, np.newaxis],
56
+ np.maximum(bbox_tl[1], candidates_tl[:, 1])[:, np.newaxis]]
57
+ br = np.c_[np.minimum(bbox_br[0], candidates_br[:, 0])[:, np.newaxis],
58
+ np.minimum(bbox_br[1], candidates_br[:, 1])[:, np.newaxis]]
59
+ wh = np.maximum(0., br - tl)
60
+
61
+ area_intersection = wh.prod(axis=1)
62
+ area_bbox = bbox[2:].prod()
63
+ area_candidates = candidates[:, 2:].prod(axis=1)
64
+ ious = area_intersection / (
65
+ area_bbox + area_candidates - area_intersection)
66
+ return ious
67
+
68
+
69
+ def iou_cost(tracks, detections, track_indices=None, detection_indices=None):
70
+ """
71
+ IoU distance metric.
72
+
73
+ Args:
74
+ tracks (list[Track]): A list of tracks.
75
+ detections (list[Detection]): A list of detections.
76
+ track_indices (Optional[list[int]]): A list of indices to tracks that
77
+ should be matched. Defaults to all `tracks`.
78
+ detection_indices (Optional[list[int]]): A list of indices to detections
79
+ that should be matched. Defaults to all `detections`.
80
+
81
+ Returns:
82
+ cost_matrix (ndarray): A cost matrix of shape len(track_indices),
83
+ len(detection_indices) where entry (i, j) is
84
+ `1 - iou(tracks[track_indices[i]], detections[detection_indices[j]])`.
85
+ """
86
+ if track_indices is None:
87
+ track_indices = np.arange(len(tracks))
88
+ if detection_indices is None:
89
+ detection_indices = np.arange(len(detections))
90
+
91
+ cost_matrix = np.zeros((len(track_indices), len(detection_indices)))
92
+ for row, track_idx in enumerate(track_indices):
93
+ if tracks[track_idx].time_since_update > 1:
94
+ cost_matrix[row, :] = 1e+5
95
+ continue
96
+
97
+ bbox = tracks[track_idx].to_tlwh()
98
+ candidates = np.asarray(
99
+ [detections[i].tlwh for i in detection_indices])
100
+ cost_matrix[row, :] = 1. - iou_1toN(bbox, candidates)
101
+ return cost_matrix
102
+
103
+
104
+ def _nn_euclidean_distance(s, q):
105
+ """
106
+ Compute pair-wise squared (Euclidean) distance between points in `s` and `q`.
107
+
108
+ Args:
109
+ s (ndarray): Sample points: an NxM matrix of N samples of dimensionality M.
110
+ q (ndarray): Query points: an LxM matrix of L samples of dimensionality M.
111
+
112
+ Returns:
113
+ distances (ndarray): A vector of length M that contains for each entry in `q` the
114
+ smallest Euclidean distance to a sample in `s`.
115
+ """
116
+ s, q = np.asarray(s), np.asarray(q)
117
+ if len(s) == 0 or len(q) == 0:
118
+ return np.zeros((len(s), len(q)))
119
+ s2, q2 = np.square(s).sum(axis=1), np.square(q).sum(axis=1)
120
+ distances = -2. * np.dot(s, q.T) + s2[:, None] + q2[None, :]
121
+ distances = np.clip(distances, 0., float(np.inf))
122
+
123
+ return np.maximum(0.0, distances.min(axis=0))
124
+
125
+
126
+ def _nn_cosine_distance(s, q):
127
+ """
128
+ Compute pair-wise cosine distance between points in `s` and `q`.
129
+
130
+ Args:
131
+ s (ndarray): Sample points: an NxM matrix of N samples of dimensionality M.
132
+ q (ndarray): Query points: an LxM matrix of L samples of dimensionality M.
133
+
134
+ Returns:
135
+ distances (ndarray): A vector of length M that contains for each entry in `q` the
136
+ smallest Euclidean distance to a sample in `s`.
137
+ """
138
+ s = np.asarray(s) / np.linalg.norm(s, axis=1, keepdims=True)
139
+ q = np.asarray(q) / np.linalg.norm(q, axis=1, keepdims=True)
140
+ distances = 1. - np.dot(s, q.T)
141
+
142
+ return distances.min(axis=0)
143
+
144
+
145
+ class NearestNeighborDistanceMetric(object):
146
+ """
147
+ A nearest neighbor distance metric that, for each target, returns
148
+ the closest distance to any sample that has been observed so far.
149
+
150
+ Args:
151
+ metric (str): Either "euclidean" or "cosine".
152
+ matching_threshold (float): The matching threshold. Samples with larger
153
+ distance are considered an invalid match.
154
+ budget (Optional[int]): If not None, fix samples per class to at most
155
+ this number. Removes the oldest samples when the budget is reached.
156
+
157
+ Attributes:
158
+ samples (Dict[int -> List[ndarray]]): A dictionary that maps from target
159
+ identities to the list of samples that have been observed so far.
160
+ """
161
+
162
+ def __init__(self, metric, matching_threshold, budget=None):
163
+ if metric == "euclidean":
164
+ self._metric = _nn_euclidean_distance
165
+ elif metric == "cosine":
166
+ self._metric = _nn_cosine_distance
167
+ else:
168
+ raise ValueError(
169
+ "Invalid metric; must be either 'euclidean' or 'cosine'")
170
+ self.matching_threshold = matching_threshold
171
+ self.budget = budget
172
+ self.samples = {}
173
+
174
+ def partial_fit(self, features, targets, active_targets):
175
+ """
176
+ Update the distance metric with new data.
177
+
178
+ Args:
179
+ features (ndarray): An NxM matrix of N features of dimensionality M.
180
+ targets (ndarray): An integer array of associated target identities.
181
+ active_targets (List[int]): A list of targets that are currently
182
+ present in the scene.
183
+ """
184
+ for feature, target in zip(features, targets):
185
+ self.samples.setdefault(target, []).append(feature)
186
+ if self.budget is not None:
187
+ self.samples[target] = self.samples[target][-self.budget:]
188
+ self.samples = {k: self.samples[k] for k in active_targets}
189
+
190
+ def distance(self, features, targets):
191
+ """
192
+ Compute distance between features and targets.
193
+
194
+ Args:
195
+ features (ndarray): An NxM matrix of N features of dimensionality M.
196
+ targets (list[int]): A list of targets to match the given `features` against.
197
+
198
+ Returns:
199
+ cost_matrix (ndarray): a cost matrix of shape len(targets), len(features),
200
+ where element (i, j) contains the closest squared distance between
201
+ `targets[i]` and `features[j]`.
202
+ """
203
+ cost_matrix = np.zeros((len(targets), len(features)))
204
+ for i, target in enumerate(targets):
205
+ cost_matrix[i, :] = self._metric(self.samples[target], features)
206
+ return cost_matrix
207
+
208
+
209
+ def min_cost_matching(distance_metric,
210
+ max_distance,
211
+ tracks,
212
+ detections,
213
+ track_indices=None,
214
+ detection_indices=None):
215
+ """
216
+ Solve linear assignment problem.
217
+
218
+ Args:
219
+ distance_metric :
220
+ Callable[List[Track], List[Detection], List[int], List[int]) -> ndarray
221
+ The distance metric is given a list of tracks and detections as
222
+ well as a list of N track indices and M detection indices. The
223
+ metric should return the NxM dimensional cost matrix, where element
224
+ (i, j) is the association cost between the i-th track in the given
225
+ track indices and the j-th detection in the given detection_indices.
226
+ max_distance (float): Gating threshold. Associations with cost larger
227
+ than this value are disregarded.
228
+ tracks (list[Track]): A list of predicted tracks at the current time
229
+ step.
230
+ detections (list[Detection]): A list of detections at the current time
231
+ step.
232
+ track_indices (list[int]): List of track indices that maps rows in
233
+ `cost_matrix` to tracks in `tracks`.
234
+ detection_indices (List[int]): List of detection indices that maps
235
+ columns in `cost_matrix` to detections in `detections`.
236
+
237
+ Returns:
238
+ A tuple (List[(int, int)], List[int], List[int]) with the following
239
+ three entries:
240
+ * A list of matched track and detection indices.
241
+ * A list of unmatched track indices.
242
+ * A list of unmatched detection indices.
243
+ """
244
+ if track_indices is None:
245
+ track_indices = np.arange(len(tracks))
246
+ if detection_indices is None:
247
+ detection_indices = np.arange(len(detections))
248
+
249
+ if len(detection_indices) == 0 or len(track_indices) == 0:
250
+ return [], track_indices, detection_indices # Nothing to match.
251
+
252
+ cost_matrix = distance_metric(tracks, detections, track_indices,
253
+ detection_indices)
254
+
255
+ cost_matrix[cost_matrix > max_distance] = max_distance + 1e-5
256
+ indices = linear_sum_assignment(cost_matrix)
257
+
258
+ matches, unmatched_tracks, unmatched_detections = [], [], []
259
+ for col, detection_idx in enumerate(detection_indices):
260
+ if col not in indices[1]:
261
+ unmatched_detections.append(detection_idx)
262
+ for row, track_idx in enumerate(track_indices):
263
+ if row not in indices[0]:
264
+ unmatched_tracks.append(track_idx)
265
+ for row, col in zip(indices[0], indices[1]):
266
+ track_idx = track_indices[row]
267
+ detection_idx = detection_indices[col]
268
+ if cost_matrix[row, col] > max_distance:
269
+ unmatched_tracks.append(track_idx)
270
+ unmatched_detections.append(detection_idx)
271
+ else:
272
+ matches.append((track_idx, detection_idx))
273
+ return matches, unmatched_tracks, unmatched_detections
274
+
275
+
276
+ def matching_cascade(distance_metric,
277
+ max_distance,
278
+ cascade_depth,
279
+ tracks,
280
+ detections,
281
+ track_indices=None,
282
+ detection_indices=None):
283
+ """
284
+ Run matching cascade.
285
+
286
+ Args:
287
+ distance_metric :
288
+ Callable[List[Track], List[Detection], List[int], List[int]) -> ndarray
289
+ The distance metric is given a list of tracks and detections as
290
+ well as a list of N track indices and M detection indices. The
291
+ metric should return the NxM dimensional cost matrix, where element
292
+ (i, j) is the association cost between the i-th track in the given
293
+ track indices and the j-th detection in the given detection_indices.
294
+ max_distance (float): Gating threshold. Associations with cost larger
295
+ than this value are disregarded.
296
+ cascade_depth (int): The cascade depth, should be se to the maximum
297
+ track age.
298
+ tracks (list[Track]): A list of predicted tracks at the current time
299
+ step.
300
+ detections (list[Detection]): A list of detections at the current time
301
+ step.
302
+ track_indices (list[int]): List of track indices that maps rows in
303
+ `cost_matrix` to tracks in `tracks`.
304
+ detection_indices (List[int]): List of detection indices that maps
305
+ columns in `cost_matrix` to detections in `detections`.
306
+
307
+ Returns:
308
+ A tuple (List[(int, int)], List[int], List[int]) with the following
309
+ three entries:
310
+ * A list of matched track and detection indices.
311
+ * A list of unmatched track indices.
312
+ * A list of unmatched detection indices.
313
+ """
314
+ if track_indices is None:
315
+ track_indices = list(range(len(tracks)))
316
+ if detection_indices is None:
317
+ detection_indices = list(range(len(detections)))
318
+
319
+ unmatched_detections = detection_indices
320
+ matches = []
321
+ for level in range(cascade_depth):
322
+ if len(unmatched_detections) == 0: # No detections left
323
+ break
324
+
325
+ track_indices_l = [
326
+ k for k in track_indices
327
+ if tracks[k].time_since_update == 1 + level
328
+ ]
329
+ if len(track_indices_l) == 0: # Nothing to match at this level
330
+ continue
331
+
332
+ matches_l, _, unmatched_detections = \
333
+ min_cost_matching(
334
+ distance_metric, max_distance, tracks, detections,
335
+ track_indices_l, unmatched_detections)
336
+ matches += matches_l
337
+ unmatched_tracks = list(set(track_indices) - set(k for k, _ in matches))
338
+ return matches, unmatched_tracks, unmatched_detections
339
+
340
+
341
+ def gate_cost_matrix(kf,
342
+ cost_matrix,
343
+ tracks,
344
+ detections,
345
+ track_indices,
346
+ detection_indices,
347
+ gated_cost=INFTY_COST,
348
+ only_position=False):
349
+ """
350
+ Invalidate infeasible entries in cost matrix based on the state
351
+ distributions obtained by Kalman filtering.
352
+
353
+ Args:
354
+ kf (object): The Kalman filter.
355
+ cost_matrix (ndarray): The NxM dimensional cost matrix, where N is the
356
+ number of track indices and M is the number of detection indices,
357
+ such that entry (i, j) is the association cost between
358
+ `tracks[track_indices[i]]` and `detections[detection_indices[j]]`.
359
+ tracks (list[Track]): A list of predicted tracks at the current time
360
+ step.
361
+ detections (list[Detection]): A list of detections at the current time
362
+ step.
363
+ track_indices (List[int]): List of track indices that maps rows in
364
+ `cost_matrix` to tracks in `tracks`.
365
+ detection_indices (List[int]): List of detection indices that maps
366
+ columns in `cost_matrix` to detections in `detections`.
367
+ gated_cost (Optional[float]): Entries in the cost matrix corresponding
368
+ to infeasible associations are set this value. Defaults to a very
369
+ large value.
370
+ only_position (Optional[bool]): If True, only the x, y position of the
371
+ state distribution is considered during gating. Default False.
372
+ """
373
+ gating_dim = 2 if only_position else 4
374
+ gating_threshold = kalman_filter.chi2inv95[gating_dim]
375
+ measurements = np.asarray(
376
+ [detections[i].to_xyah() for i in detection_indices])
377
+ for row, track_idx in enumerate(track_indices):
378
+ track = tracks[track_idx]
379
+ gating_distance = kf.gating_distance(track.mean, track.covariance,
380
+ measurements, only_position)
381
+ cost_matrix[row, gating_distance > gating_threshold] = gated_cost
382
+ return cost_matrix
pptracking/python/mot/matching/jde_matching.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/Zhongdao/Towards-Realtime-MOT/blob/master/tracker/matching.py
16
+ """
17
+
18
+ try:
19
+ import lap
20
+ except:
21
+ print(
22
+ 'Warning: Unable to use JDE/FairMOT/ByteTrack, please install lap, for example: `pip install lap`, see https://github.com/gatagat/lap'
23
+ )
24
+ pass
25
+
26
+ import scipy
27
+ import numpy as np
28
+ from scipy.spatial.distance import cdist
29
+ from ..motion import kalman_filter
30
+ import warnings
31
+ warnings.filterwarnings("ignore")
32
+
33
+ __all__ = [
34
+ 'merge_matches',
35
+ 'linear_assignment',
36
+ 'bbox_ious',
37
+ 'iou_distance',
38
+ 'embedding_distance',
39
+ 'fuse_motion',
40
+ ]
41
+
42
+
43
+ def merge_matches(m1, m2, shape):
44
+ O, P, Q = shape
45
+ m1 = np.asarray(m1)
46
+ m2 = np.asarray(m2)
47
+
48
+ M1 = scipy.sparse.coo_matrix(
49
+ (np.ones(len(m1)), (m1[:, 0], m1[:, 1])), shape=(O, P))
50
+ M2 = scipy.sparse.coo_matrix(
51
+ (np.ones(len(m2)), (m2[:, 0], m2[:, 1])), shape=(P, Q))
52
+
53
+ mask = M1 * M2
54
+ match = mask.nonzero()
55
+ match = list(zip(match[0], match[1]))
56
+ unmatched_O = tuple(set(range(O)) - set([i for i, j in match]))
57
+ unmatched_Q = tuple(set(range(Q)) - set([j for i, j in match]))
58
+
59
+ return match, unmatched_O, unmatched_Q
60
+
61
+
62
+ def linear_assignment(cost_matrix, thresh):
63
+ try:
64
+ import lap
65
+ except Exception as e:
66
+ raise RuntimeError(
67
+ 'Unable to use JDE/FairMOT/ByteTrack, please install lap, for example: `pip install lap`, see https://github.com/gatagat/lap'
68
+ )
69
+ if cost_matrix.size == 0:
70
+ return np.empty(
71
+ (0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(
72
+ range(cost_matrix.shape[1]))
73
+ matches, unmatched_a, unmatched_b = [], [], []
74
+ cost, x, y = lap.lapjv(cost_matrix, extend_cost=True, cost_limit=thresh)
75
+ for ix, mx in enumerate(x):
76
+ if mx >= 0:
77
+ matches.append([ix, mx])
78
+ unmatched_a = np.where(x < 0)[0]
79
+ unmatched_b = np.where(y < 0)[0]
80
+ matches = np.asarray(matches)
81
+ return matches, unmatched_a, unmatched_b
82
+
83
+
84
+ def bbox_ious(atlbrs, btlbrs):
85
+ boxes = np.ascontiguousarray(atlbrs, dtype=np.float)
86
+ query_boxes = np.ascontiguousarray(btlbrs, dtype=np.float)
87
+ N = boxes.shape[0]
88
+ K = query_boxes.shape[0]
89
+ ious = np.zeros((N, K), dtype=boxes.dtype)
90
+ if N * K == 0:
91
+ return ious
92
+
93
+ for k in range(K):
94
+ box_area = ((query_boxes[k, 2] - query_boxes[k, 0] + 1) *
95
+ (query_boxes[k, 3] - query_boxes[k, 1] + 1))
96
+ for n in range(N):
97
+ iw = (min(boxes[n, 2], query_boxes[k, 2]) - max(
98
+ boxes[n, 0], query_boxes[k, 0]) + 1)
99
+ if iw > 0:
100
+ ih = (min(boxes[n, 3], query_boxes[k, 3]) - max(
101
+ boxes[n, 1], query_boxes[k, 1]) + 1)
102
+ if ih > 0:
103
+ ua = float((boxes[n, 2] - boxes[n, 0] + 1) * (boxes[
104
+ n, 3] - boxes[n, 1] + 1) + box_area - iw * ih)
105
+ ious[n, k] = iw * ih / ua
106
+ return ious
107
+
108
+
109
+ def iou_distance(atracks, btracks):
110
+ """
111
+ Compute cost based on IoU between two list[STrack].
112
+ """
113
+ if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or (
114
+ len(btracks) > 0 and isinstance(btracks[0], np.ndarray)):
115
+ atlbrs = atracks
116
+ btlbrs = btracks
117
+ else:
118
+ atlbrs = [track.tlbr for track in atracks]
119
+ btlbrs = [track.tlbr for track in btracks]
120
+ _ious = bbox_ious(atlbrs, btlbrs)
121
+ cost_matrix = 1 - _ious
122
+
123
+ return cost_matrix
124
+
125
+
126
+ def embedding_distance(tracks, detections, metric='euclidean'):
127
+ """
128
+ Compute cost based on features between two list[STrack].
129
+ """
130
+ cost_matrix = np.zeros((len(tracks), len(detections)), dtype=np.float)
131
+ if cost_matrix.size == 0:
132
+ return cost_matrix
133
+ det_features = np.asarray(
134
+ [track.curr_feat for track in detections], dtype=np.float)
135
+ track_features = np.asarray(
136
+ [track.smooth_feat for track in tracks], dtype=np.float)
137
+ cost_matrix = np.maximum(0.0, cdist(track_features, det_features,
138
+ metric)) # Nomalized features
139
+ return cost_matrix
140
+
141
+
142
+ def fuse_motion(kf,
143
+ cost_matrix,
144
+ tracks,
145
+ detections,
146
+ only_position=False,
147
+ lambda_=0.98):
148
+ if cost_matrix.size == 0:
149
+ return cost_matrix
150
+ gating_dim = 2 if only_position else 4
151
+ gating_threshold = kalman_filter.chi2inv95[gating_dim]
152
+ measurements = np.asarray([det.to_xyah() for det in detections])
153
+ for row, track in enumerate(tracks):
154
+ gating_distance = kf.gating_distance(
155
+ track.mean,
156
+ track.covariance,
157
+ measurements,
158
+ only_position,
159
+ metric='maha')
160
+ cost_matrix[row, gating_distance > gating_threshold] = np.inf
161
+ cost_matrix[row] = lambda_ * cost_matrix[row] + (1 - lambda_
162
+ ) * gating_distance
163
+ return cost_matrix
pptracking/python/mot/matching/ocsort_matching.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/noahcao/OC_SORT/blob/master/trackers/ocsort_tracker/association.py
16
+ """
17
+
18
+ import os
19
+ import numpy as np
20
+
21
+
22
+ def iou_batch(bboxes1, bboxes2):
23
+ """
24
+ From SORT: Computes IOU between two bboxes in the form [x1,y1,x2,y2]
25
+ """
26
+ bboxes2 = np.expand_dims(bboxes2, 0)
27
+ bboxes1 = np.expand_dims(bboxes1, 1)
28
+
29
+ xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0])
30
+ yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1])
31
+ xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2])
32
+ yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3])
33
+ w = np.maximum(0., xx2 - xx1)
34
+ h = np.maximum(0., yy2 - yy1)
35
+ wh = w * h
36
+ o = wh / ((bboxes1[..., 2] - bboxes1[..., 0]) *
37
+ (bboxes1[..., 3] - bboxes1[..., 1]) +
38
+ (bboxes2[..., 2] - bboxes2[..., 0]) *
39
+ (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
40
+ return (o)
41
+
42
+
43
+ def speed_direction_batch(dets, tracks):
44
+ tracks = tracks[..., np.newaxis]
45
+ CX1, CY1 = (dets[:, 0] + dets[:, 2]) / 2.0, (dets[:, 1] + dets[:, 3]) / 2.0
46
+ CX2, CY2 = (tracks[:, 0] + tracks[:, 2]) / 2.0, (
47
+ tracks[:, 1] + tracks[:, 3]) / 2.0
48
+ dx = CX1 - CX2
49
+ dy = CY1 - CY2
50
+ norm = np.sqrt(dx**2 + dy**2) + 1e-6
51
+ dx = dx / norm
52
+ dy = dy / norm
53
+ return dy, dx # size: num_track x num_det
54
+
55
+
56
+ def linear_assignment(cost_matrix):
57
+ try:
58
+ import lap
59
+ _, x, y = lap.lapjv(cost_matrix, extend_cost=True)
60
+ return np.array([[y[i], i] for i in x if i >= 0]) #
61
+ except ImportError:
62
+ from scipy.optimize import linear_sum_assignment
63
+ x, y = linear_sum_assignment(cost_matrix)
64
+ return np.array(list(zip(x, y)))
65
+
66
+
67
+ def associate(detections, trackers, iou_threshold, velocities, previous_obs,
68
+ vdc_weight):
69
+ if (len(trackers) == 0):
70
+ return np.empty(
71
+ (0, 2), dtype=int), np.arange(len(detections)), np.empty(
72
+ (0, 5), dtype=int)
73
+
74
+ Y, X = speed_direction_batch(detections, previous_obs)
75
+ inertia_Y, inertia_X = velocities[:, 0], velocities[:, 1]
76
+ inertia_Y = np.repeat(inertia_Y[:, np.newaxis], Y.shape[1], axis=1)
77
+ inertia_X = np.repeat(inertia_X[:, np.newaxis], X.shape[1], axis=1)
78
+ diff_angle_cos = inertia_X * X + inertia_Y * Y
79
+ diff_angle_cos = np.clip(diff_angle_cos, a_min=-1, a_max=1)
80
+ diff_angle = np.arccos(diff_angle_cos)
81
+ diff_angle = (np.pi / 2.0 - np.abs(diff_angle)) / np.pi
82
+
83
+ valid_mask = np.ones(previous_obs.shape[0])
84
+ valid_mask[np.where(previous_obs[:, 4] < 0)] = 0
85
+
86
+ iou_matrix = iou_batch(detections, trackers)
87
+ scores = np.repeat(
88
+ detections[:, -1][:, np.newaxis], trackers.shape[0], axis=1)
89
+ # iou_matrix = iou_matrix * scores # a trick sometiems works, we don't encourage this
90
+ valid_mask = np.repeat(valid_mask[:, np.newaxis], X.shape[1], axis=1)
91
+
92
+ angle_diff_cost = (valid_mask * diff_angle) * vdc_weight
93
+ angle_diff_cost = angle_diff_cost.T
94
+ angle_diff_cost = angle_diff_cost * scores
95
+
96
+ if min(iou_matrix.shape) > 0:
97
+ a = (iou_matrix > iou_threshold).astype(np.int32)
98
+ if a.sum(1).max() == 1 and a.sum(0).max() == 1:
99
+ matched_indices = np.stack(np.where(a), axis=1)
100
+ else:
101
+ matched_indices = linear_assignment(-(iou_matrix + angle_diff_cost
102
+ ))
103
+ else:
104
+ matched_indices = np.empty(shape=(0, 2))
105
+
106
+ unmatched_detections = []
107
+ for d, det in enumerate(detections):
108
+ if (d not in matched_indices[:, 0]):
109
+ unmatched_detections.append(d)
110
+ unmatched_trackers = []
111
+ for t, trk in enumerate(trackers):
112
+ if (t not in matched_indices[:, 1]):
113
+ unmatched_trackers.append(t)
114
+
115
+ # filter out matched with low IOU
116
+ matches = []
117
+ for m in matched_indices:
118
+ if (iou_matrix[m[0], m[1]] < iou_threshold):
119
+ unmatched_detections.append(m[0])
120
+ unmatched_trackers.append(m[1])
121
+ else:
122
+ matches.append(m.reshape(1, 2))
123
+ if (len(matches) == 0):
124
+ matches = np.empty((0, 2), dtype=int)
125
+ else:
126
+ matches = np.concatenate(matches, axis=0)
127
+
128
+ return matches, np.array(unmatched_detections), np.array(
129
+ unmatched_trackers)
pptracking/python/mot/motion/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import kalman_filter
16
+
17
+ from .kalman_filter import *
pptracking/python/mot/motion/kalman_filter.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/nwojke/deep_sort/blob/master/deep_sort/kalman_filter.py
16
+ """
17
+
18
+ import numpy as np
19
+ import scipy.linalg
20
+
21
+ __all__ = ['KalmanFilter']
22
+ """
23
+ Table for the 0.95 quantile of the chi-square distribution with N degrees of
24
+ freedom (contains values for N=1, ..., 9). Taken from MATLAB/Octave's chi2inv
25
+ function and used as Mahalanobis gating threshold.
26
+ """
27
+
28
+ chi2inv95 = {
29
+ 1: 3.8415,
30
+ 2: 5.9915,
31
+ 3: 7.8147,
32
+ 4: 9.4877,
33
+ 5: 11.070,
34
+ 6: 12.592,
35
+ 7: 14.067,
36
+ 8: 15.507,
37
+ 9: 16.919
38
+ }
39
+
40
+
41
+ class KalmanFilter(object):
42
+ """
43
+ A simple Kalman filter for tracking bounding boxes in image space.
44
+
45
+ The 8-dimensional state space
46
+
47
+ x, y, a, h, vx, vy, va, vh
48
+
49
+ contains the bounding box center position (x, y), aspect ratio a, height h,
50
+ and their respective velocities.
51
+
52
+ Object motion follows a constant velocity model. The bounding box location
53
+ (x, y, a, h) is taken as direct observation of the state space (linear
54
+ observation model).
55
+
56
+ """
57
+
58
+ def __init__(self):
59
+ ndim, dt = 4, 1.
60
+
61
+ # Create Kalman filter model matrices.
62
+ self._motion_mat = np.eye(2 * ndim, 2 * ndim)
63
+ for i in range(ndim):
64
+ self._motion_mat[i, ndim + i] = dt
65
+ self._update_mat = np.eye(ndim, 2 * ndim)
66
+
67
+ # Motion and observation uncertainty are chosen relative to the current
68
+ # state estimate. These weights control the amount of uncertainty in
69
+ # the model. This is a bit hacky.
70
+ self._std_weight_position = 1. / 20
71
+ self._std_weight_velocity = 1. / 160
72
+
73
+ def initiate(self, measurement):
74
+ """
75
+ Create track from unassociated measurement.
76
+
77
+ Args:
78
+ measurement (ndarray): Bounding box coordinates (x, y, a, h) with
79
+ center position (x, y), aspect ratio a, and height h.
80
+
81
+ Returns:
82
+ The mean vector (8 dimensional) and covariance matrix (8x8
83
+ dimensional) of the new track. Unobserved velocities are
84
+ initialized to 0 mean.
85
+ """
86
+ mean_pos = measurement
87
+ mean_vel = np.zeros_like(mean_pos)
88
+ mean = np.r_[mean_pos, mean_vel]
89
+
90
+ std = [
91
+ 2 * self._std_weight_position * measurement[3],
92
+ 2 * self._std_weight_position * measurement[3], 1e-2,
93
+ 2 * self._std_weight_position * measurement[3],
94
+ 10 * self._std_weight_velocity * measurement[3],
95
+ 10 * self._std_weight_velocity * measurement[3], 1e-5,
96
+ 10 * self._std_weight_velocity * measurement[3]
97
+ ]
98
+ covariance = np.diag(np.square(std))
99
+ return mean, covariance
100
+
101
+ def predict(self, mean, covariance):
102
+ """
103
+ Run Kalman filter prediction step.
104
+
105
+ Args:
106
+ mean (ndarray): The 8 dimensional mean vector of the object state
107
+ at the previous time step.
108
+ covariance (ndarray): The 8x8 dimensional covariance matrix of the
109
+ object state at the previous time step.
110
+
111
+ Returns:
112
+ The mean vector and covariance matrix of the predicted state.
113
+ Unobserved velocities are initialized to 0 mean.
114
+ """
115
+ std_pos = [
116
+ self._std_weight_position * mean[3], self._std_weight_position *
117
+ mean[3], 1e-2, self._std_weight_position * mean[3]
118
+ ]
119
+ std_vel = [
120
+ self._std_weight_velocity * mean[3], self._std_weight_velocity *
121
+ mean[3], 1e-5, self._std_weight_velocity * mean[3]
122
+ ]
123
+ motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
124
+
125
+ #mean = np.dot(self._motion_mat, mean)
126
+ mean = np.dot(mean, self._motion_mat.T)
127
+ covariance = np.linalg.multi_dot(
128
+ (self._motion_mat, covariance, self._motion_mat.T)) + motion_cov
129
+
130
+ return mean, covariance
131
+
132
+ def project(self, mean, covariance):
133
+ """
134
+ Project state distribution to measurement space.
135
+
136
+ Args
137
+ mean (ndarray): The state's mean vector (8 dimensional array).
138
+ covariance (ndarray): The state's covariance matrix (8x8 dimensional).
139
+
140
+ Returns:
141
+ The projected mean and covariance matrix of the given state estimate.
142
+ """
143
+ std = [
144
+ self._std_weight_position * mean[3], self._std_weight_position *
145
+ mean[3], 1e-1, self._std_weight_position * mean[3]
146
+ ]
147
+ innovation_cov = np.diag(np.square(std))
148
+
149
+ mean = np.dot(self._update_mat, mean)
150
+ covariance = np.linalg.multi_dot((self._update_mat, covariance,
151
+ self._update_mat.T))
152
+ return mean, covariance + innovation_cov
153
+
154
+ def multi_predict(self, mean, covariance):
155
+ """
156
+ Run Kalman filter prediction step (Vectorized version).
157
+
158
+ Args:
159
+ mean (ndarray): The Nx8 dimensional mean matrix of the object states
160
+ at the previous time step.
161
+ covariance (ndarray): The Nx8x8 dimensional covariance matrics of the
162
+ object states at the previous time step.
163
+
164
+ Returns:
165
+ The mean vector and covariance matrix of the predicted state.
166
+ Unobserved velocities are initialized to 0 mean.
167
+ """
168
+ std_pos = [
169
+ self._std_weight_position * mean[:, 3], self._std_weight_position *
170
+ mean[:, 3], 1e-2 * np.ones_like(mean[:, 3]),
171
+ self._std_weight_position * mean[:, 3]
172
+ ]
173
+ std_vel = [
174
+ self._std_weight_velocity * mean[:, 3], self._std_weight_velocity *
175
+ mean[:, 3], 1e-5 * np.ones_like(mean[:, 3]),
176
+ self._std_weight_velocity * mean[:, 3]
177
+ ]
178
+ sqr = np.square(np.r_[std_pos, std_vel]).T
179
+
180
+ motion_cov = []
181
+ for i in range(len(mean)):
182
+ motion_cov.append(np.diag(sqr[i]))
183
+ motion_cov = np.asarray(motion_cov)
184
+
185
+ mean = np.dot(mean, self._motion_mat.T)
186
+ left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
187
+ covariance = np.dot(left, self._motion_mat.T) + motion_cov
188
+
189
+ return mean, covariance
190
+
191
+ def update(self, mean, covariance, measurement):
192
+ """
193
+ Run Kalman filter correction step.
194
+
195
+ Args:
196
+ mean (ndarray): The predicted state's mean vector (8 dimensional).
197
+ covariance (ndarray): The state's covariance matrix (8x8 dimensional).
198
+ measurement (ndarray): The 4 dimensional measurement vector
199
+ (x, y, a, h), where (x, y) is the center position, a the aspect
200
+ ratio, and h the height of the bounding box.
201
+
202
+ Returns:
203
+ The measurement-corrected state distribution.
204
+ """
205
+ projected_mean, projected_cov = self.project(mean, covariance)
206
+
207
+ chol_factor, lower = scipy.linalg.cho_factor(
208
+ projected_cov, lower=True, check_finite=False)
209
+ kalman_gain = scipy.linalg.cho_solve(
210
+ (chol_factor, lower),
211
+ np.dot(covariance, self._update_mat.T).T,
212
+ check_finite=False).T
213
+ innovation = measurement - projected_mean
214
+
215
+ new_mean = mean + np.dot(innovation, kalman_gain.T)
216
+ new_covariance = covariance - np.linalg.multi_dot(
217
+ (kalman_gain, projected_cov, kalman_gain.T))
218
+ return new_mean, new_covariance
219
+
220
+ def gating_distance(self,
221
+ mean,
222
+ covariance,
223
+ measurements,
224
+ only_position=False,
225
+ metric='maha'):
226
+ """
227
+ Compute gating distance between state distribution and measurements.
228
+ A suitable distance threshold can be obtained from `chi2inv95`. If
229
+ `only_position` is False, the chi-square distribution has 4 degrees of
230
+ freedom, otherwise 2.
231
+
232
+ Args:
233
+ mean (ndarray): Mean vector over the state distribution (8
234
+ dimensional).
235
+ covariance (ndarray): Covariance of the state distribution (8x8
236
+ dimensional).
237
+ measurements (ndarray): An Nx4 dimensional matrix of N measurements,
238
+ each in format (x, y, a, h) where (x, y) is the bounding box center
239
+ position, a the aspect ratio, and h the height.
240
+ only_position (Optional[bool]): If True, distance computation is
241
+ done with respect to the bounding box center position only.
242
+ metric (str): Metric type, 'gaussian' or 'maha'.
243
+
244
+ Returns
245
+ An array of length N, where the i-th element contains the squared
246
+ Mahalanobis distance between (mean, covariance) and `measurements[i]`.
247
+ """
248
+ mean, covariance = self.project(mean, covariance)
249
+ if only_position:
250
+ mean, covariance = mean[:2], covariance[:2, :2]
251
+ measurements = measurements[:, :2]
252
+
253
+ d = measurements - mean
254
+ if metric == 'gaussian':
255
+ return np.sum(d * d, axis=1)
256
+ elif metric == 'maha':
257
+ cholesky_factor = np.linalg.cholesky(covariance)
258
+ z = scipy.linalg.solve_triangular(
259
+ cholesky_factor,
260
+ d.T,
261
+ lower=True,
262
+ check_finite=False,
263
+ overwrite_b=True)
264
+ squared_maha = np.sum(z * z, axis=0)
265
+ return squared_maha
266
+ else:
267
+ raise ValueError('invalid distance metric')
pptracking/python/mot/mtmct/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import utils
16
+ from . import postprocess
17
+ from .utils import *
18
+ from .postprocess import *
19
+
20
+ # The following codes are strongly related to zone and camera parameters
21
+ from . import camera_utils
22
+ from . import zone
23
+ from .camera_utils import *
24
+ from .zone import *
pptracking/python/mot/mtmct/camera_utils.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/LCFractal/AIC21-MTMC/tree/main/reid/reid-matching/tools
16
+
17
+ Note: The following codes are strongly related to camera parameters of the AIC21 test-set S06,
18
+ so they can only be used in S06, and can not be used for other MTMCT datasets.
19
+ """
20
+
21
+ import numpy as np
22
+ try:
23
+ from sklearn.cluster import AgglomerativeClustering
24
+ except:
25
+ print(
26
+ 'Warning: Unable to use MTMCT in PP-Tracking, please install sklearn, for example: `pip install sklearn`'
27
+ )
28
+ pass
29
+ from .utils import get_dire, get_match, get_cid_tid, combin_feature, combin_cluster
30
+ from .utils import normalize, intracam_ignore, visual_rerank
31
+
32
+ __all__ = [
33
+ 'st_filter',
34
+ 'get_labels_with_camera',
35
+ ]
36
+
37
+ CAM_DIST = [[0, 40, 55, 100, 120, 145], [40, 0, 15, 60, 80, 105],
38
+ [55, 15, 0, 40, 65, 90], [100, 60, 40, 0, 20, 45],
39
+ [120, 80, 65, 20, 0, 25], [145, 105, 90, 45, 25, 0]]
40
+
41
+
42
+ def st_filter(st_mask, cid_tids, cid_tid_dict):
43
+ count = len(cid_tids)
44
+ for i in range(count):
45
+ i_tracklet = cid_tid_dict[cid_tids[i]]
46
+ i_cid = i_tracklet['cam']
47
+ i_dire = get_dire(i_tracklet['zone_list'], i_cid)
48
+ i_iot = i_tracklet['io_time']
49
+ for j in range(count):
50
+ j_tracklet = cid_tid_dict[cid_tids[j]]
51
+ j_cid = j_tracklet['cam']
52
+ j_dire = get_dire(j_tracklet['zone_list'], j_cid)
53
+ j_iot = j_tracklet['io_time']
54
+
55
+ match_dire = True
56
+ cam_dist = CAM_DIST[i_cid - 41][j_cid - 41]
57
+ # if time overlopped
58
+ if i_iot[0] - cam_dist < j_iot[0] and j_iot[0] < i_iot[
59
+ 1] + cam_dist:
60
+ match_dire = False
61
+ if i_iot[0] - cam_dist < j_iot[1] and j_iot[1] < i_iot[
62
+ 1] + cam_dist:
63
+ match_dire = False
64
+
65
+ # not match after go out
66
+ if i_dire[1] in [1, 2]: # i out
67
+ if i_iot[0] < j_iot[1] + cam_dist:
68
+ match_dire = False
69
+
70
+ if i_dire[1] in [1, 2]:
71
+ if i_dire[0] in [3] and i_cid > j_cid:
72
+ match_dire = False
73
+ if i_dire[0] in [4] and i_cid < j_cid:
74
+ match_dire = False
75
+
76
+ if i_cid in [41] and i_dire[1] in [4]:
77
+ if i_iot[0] < j_iot[1] + cam_dist:
78
+ match_dire = False
79
+ if i_iot[1] > 199:
80
+ match_dire = False
81
+ if i_cid in [46] and i_dire[1] in [3]:
82
+ if i_iot[0] < j_iot[1] + cam_dist:
83
+ match_dire = False
84
+
85
+ # match after come into
86
+ if i_dire[0] in [1, 2]:
87
+ if i_iot[1] > j_iot[0] - cam_dist:
88
+ match_dire = False
89
+
90
+ if i_dire[0] in [1, 2]:
91
+ if i_dire[1] in [3] and i_cid > j_cid:
92
+ match_dire = False
93
+ if i_dire[1] in [4] and i_cid < j_cid:
94
+ match_dire = False
95
+
96
+ is_ignore = False
97
+ if ((i_dire[0] == i_dire[1] and i_dire[0] in [3, 4]) or
98
+ (j_dire[0] == j_dire[1] and j_dire[0] in [3, 4])):
99
+ is_ignore = True
100
+
101
+ if not is_ignore:
102
+ # direction conflict
103
+ if (i_dire[0] in [3] and j_dire[0] in [4]) or (
104
+ i_dire[1] in [3] and j_dire[1] in [4]):
105
+ match_dire = False
106
+ # filter before going next scene
107
+ if i_dire[1] in [3] and i_cid < j_cid:
108
+ if i_iot[1] > j_iot[1] - cam_dist:
109
+ match_dire = False
110
+ if i_dire[1] in [4] and i_cid > j_cid:
111
+ if i_iot[1] > j_iot[1] - cam_dist:
112
+ match_dire = False
113
+
114
+ if i_dire[0] in [3] and i_cid < j_cid:
115
+ if i_iot[0] < j_iot[0] + cam_dist:
116
+ match_dire = False
117
+ if i_dire[0] in [4] and i_cid > j_cid:
118
+ if i_iot[0] < j_iot[0] + cam_dist:
119
+ match_dire = False
120
+ ## 3-30
121
+ ## 4-1
122
+ if i_dire[0] in [3] and i_cid > j_cid:
123
+ if i_iot[1] > j_iot[0] - cam_dist:
124
+ match_dire = False
125
+ if i_dire[0] in [4] and i_cid < j_cid:
126
+ if i_iot[1] > j_iot[0] - cam_dist:
127
+ match_dire = False
128
+ # filter before going next scene
129
+ ## 4-7
130
+ if i_dire[1] in [3] and i_cid > j_cid:
131
+ if i_iot[0] < j_iot[1] + cam_dist:
132
+ match_dire = False
133
+ if i_dire[1] in [4] and i_cid < j_cid:
134
+ if i_iot[0] < j_iot[1] + cam_dist:
135
+ match_dire = False
136
+ else:
137
+ if i_iot[1] > 199:
138
+ if i_dire[0] in [3] and i_cid < j_cid:
139
+ if i_iot[0] < j_iot[0] + cam_dist:
140
+ match_dire = False
141
+ if i_dire[0] in [4] and i_cid > j_cid:
142
+ if i_iot[0] < j_iot[0] + cam_dist:
143
+ match_dire = False
144
+ if i_dire[0] in [3] and i_cid > j_cid:
145
+ match_dire = False
146
+ if i_dire[0] in [4] and i_cid < j_cid:
147
+ match_dire = False
148
+ if i_iot[0] < 1:
149
+ if i_dire[1] in [3] and i_cid > j_cid:
150
+ match_dire = False
151
+ if i_dire[1] in [4] and i_cid < j_cid:
152
+ match_dire = False
153
+
154
+ if not match_dire:
155
+ st_mask[i, j] = 0.0
156
+ st_mask[j, i] = 0.0
157
+ return st_mask
158
+
159
+
160
+ def subcam_list(cid_tid_dict, cid_tids):
161
+ sub_3_4 = dict()
162
+ sub_4_3 = dict()
163
+ for cid_tid in cid_tids:
164
+ cid, tid = cid_tid
165
+ tracklet = cid_tid_dict[cid_tid]
166
+ zs, ze = get_dire(tracklet['zone_list'], cid)
167
+ if zs in [3] and cid not in [46]: # 4 to 3
168
+ if not cid + 1 in sub_4_3:
169
+ sub_4_3[cid + 1] = []
170
+ sub_4_3[cid + 1].append(cid_tid)
171
+ if ze in [4] and cid not in [41]: # 4 to 3
172
+ if not cid in sub_4_3:
173
+ sub_4_3[cid] = []
174
+ sub_4_3[cid].append(cid_tid)
175
+ if zs in [4] and cid not in [41]: # 3 to 4
176
+ if not cid - 1 in sub_3_4:
177
+ sub_3_4[cid - 1] = []
178
+ sub_3_4[cid - 1].append(cid_tid)
179
+ if ze in [3] and cid not in [46]: # 3 to 4
180
+ if not cid in sub_3_4:
181
+ sub_3_4[cid] = []
182
+ sub_3_4[cid].append(cid_tid)
183
+ sub_cid_tids = dict()
184
+ for i in sub_3_4:
185
+ sub_cid_tids[(i, i + 1)] = sub_3_4[i]
186
+ for i in sub_4_3:
187
+ sub_cid_tids[(i, i - 1)] = sub_4_3[i]
188
+ return sub_cid_tids
189
+
190
+
191
+ def subcam_list2(cid_tid_dict, cid_tids):
192
+ sub_dict = dict()
193
+ for cid_tid in cid_tids:
194
+ cid, tid = cid_tid
195
+ if cid not in [41]:
196
+ if not cid in sub_dict:
197
+ sub_dict[cid] = []
198
+ sub_dict[cid].append(cid_tid)
199
+ if cid not in [46]:
200
+ if not cid + 1 in sub_dict:
201
+ sub_dict[cid + 1] = []
202
+ sub_dict[cid + 1].append(cid_tid)
203
+ return sub_dict
204
+
205
+
206
+ def get_sim_matrix(cid_tid_dict,
207
+ cid_tids,
208
+ use_ff=True,
209
+ use_rerank=True,
210
+ use_st_filter=False):
211
+ # Note: camera releated get_sim_matrix function,
212
+ # which is different from the one in utils.py.
213
+ count = len(cid_tids)
214
+
215
+ q_arr = np.array(
216
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
217
+ g_arr = np.array(
218
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
219
+ q_arr = normalize(q_arr, axis=1)
220
+ g_arr = normalize(g_arr, axis=1)
221
+
222
+ st_mask = np.ones((count, count), dtype=np.float32)
223
+ st_mask = intracam_ignore(st_mask, cid_tids)
224
+
225
+ # different from utils.py
226
+ if use_st_filter:
227
+ st_mask = st_filter(st_mask, cid_tids, cid_tid_dict)
228
+
229
+ visual_sim_matrix = visual_rerank(
230
+ q_arr, g_arr, cid_tids, use_ff=use_ff, use_rerank=use_rerank)
231
+ visual_sim_matrix = visual_sim_matrix.astype('float32')
232
+
233
+ np.set_printoptions(precision=3)
234
+ sim_matrix = visual_sim_matrix * st_mask
235
+
236
+ np.fill_diagonal(sim_matrix, 0)
237
+ return sim_matrix
238
+
239
+
240
+ def get_labels_with_camera(cid_tid_dict,
241
+ cid_tids,
242
+ use_ff=True,
243
+ use_rerank=True,
244
+ use_st_filter=False):
245
+ # 1st cluster
246
+ sub_cid_tids = subcam_list(cid_tid_dict, cid_tids)
247
+ sub_labels = dict()
248
+ dis_thrs = [0.7, 0.5, 0.5, 0.5, 0.5, 0.7, 0.5, 0.5, 0.5, 0.5]
249
+
250
+ for i, sub_c_to_c in enumerate(sub_cid_tids):
251
+ sim_matrix = get_sim_matrix(
252
+ cid_tid_dict,
253
+ sub_cid_tids[sub_c_to_c],
254
+ use_ff=use_ff,
255
+ use_rerank=use_rerank,
256
+ use_st_filter=use_st_filter)
257
+ cluster_labels = AgglomerativeClustering(
258
+ n_clusters=None,
259
+ distance_threshold=1 - dis_thrs[i],
260
+ affinity='precomputed',
261
+ linkage='complete').fit_predict(1 - sim_matrix)
262
+ labels = get_match(cluster_labels)
263
+ cluster_cid_tids = get_cid_tid(labels, sub_cid_tids[sub_c_to_c])
264
+ sub_labels[sub_c_to_c] = cluster_cid_tids
265
+ labels, sub_cluster = combin_cluster(sub_labels, cid_tids)
266
+
267
+ # 2nd cluster
268
+ cid_tid_dict_new = combin_feature(cid_tid_dict, sub_cluster)
269
+ sub_cid_tids = subcam_list2(cid_tid_dict_new, cid_tids)
270
+ sub_labels = dict()
271
+ for i, sub_c_to_c in enumerate(sub_cid_tids):
272
+ sim_matrix = get_sim_matrix(
273
+ cid_tid_dict_new,
274
+ sub_cid_tids[sub_c_to_c],
275
+ use_ff=use_ff,
276
+ use_rerank=use_rerank,
277
+ use_st_filter=use_st_filter)
278
+ cluster_labels = AgglomerativeClustering(
279
+ n_clusters=None,
280
+ distance_threshold=1 - 0.1,
281
+ affinity='precomputed',
282
+ linkage='complete').fit_predict(1 - sim_matrix)
283
+ labels = get_match(cluster_labels)
284
+ cluster_cid_tids = get_cid_tid(labels, sub_cid_tids[sub_c_to_c])
285
+ sub_labels[sub_c_to_c] = cluster_cid_tids
286
+ labels, sub_cluster = combin_cluster(sub_labels, cid_tids)
287
+
288
+ return labels
pptracking/python/mot/mtmct/postprocess.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/LCFractal/AIC21-MTMC/tree/main/reid/reid-matching/tools
16
+ """
17
+
18
+ import os
19
+ import re
20
+ import cv2
21
+ from tqdm import tqdm
22
+ import numpy as np
23
+ try:
24
+ import motmetrics as mm
25
+ except:
26
+ print(
27
+ 'Warning: Unable to use motmetrics in MTMCT in PP-Tracking, please install motmetrics, for example: `pip install motmetrics`, see https://github.com/longcw/py-motmetrics'
28
+ )
29
+ pass
30
+ from functools import reduce
31
+
32
+ from .utils import parse_pt_gt, parse_pt, compare_dataframes_mtmc
33
+ from .utils import get_labels, getData, gen_new_mot
34
+ from .camera_utils import get_labels_with_camera
35
+ from .zone import Zone
36
+ from ..visualize import plot_tracking
37
+
38
+ __all__ = [
39
+ 'trajectory_fusion',
40
+ 'sub_cluster',
41
+ 'gen_res',
42
+ 'print_mtmct_result',
43
+ 'get_mtmct_matching_results',
44
+ 'save_mtmct_crops',
45
+ 'save_mtmct_vis_results',
46
+ ]
47
+
48
+
49
+ def trajectory_fusion(mot_feature, cid, cid_bias, use_zone=False,
50
+ zone_path=''):
51
+ cur_bias = cid_bias[cid]
52
+ mot_list_break = {}
53
+ if use_zone:
54
+ zones = Zone(zone_path=zone_path)
55
+ zones.set_cam(cid)
56
+ mot_list = parse_pt(mot_feature, zones)
57
+ else:
58
+ mot_list = parse_pt(mot_feature)
59
+
60
+ if use_zone:
61
+ mot_list = zones.break_mot(mot_list, cid)
62
+ mot_list = zones.filter_mot(mot_list, cid) # filter by zone
63
+ mot_list = zones.filter_bbox(mot_list, cid) # filter bbox
64
+
65
+ mot_list_break = gen_new_mot(mot_list) # save break feature for gen result
66
+
67
+ tid_data = dict()
68
+ for tid in mot_list:
69
+ tracklet = mot_list[tid]
70
+ if len(tracklet) <= 1:
71
+ continue
72
+ frame_list = list(tracklet.keys())
73
+ frame_list.sort()
74
+ # filter area too large
75
+ zone_list = [tracklet[f]['zone'] for f in frame_list]
76
+ feature_list = [
77
+ tracklet[f]['feat'] for f in frame_list
78
+ if (tracklet[f]['bbox'][3] - tracklet[f]['bbox'][1]
79
+ ) * (tracklet[f]['bbox'][2] - tracklet[f]['bbox'][0]) > 2000
80
+ ]
81
+ if len(feature_list) < 2:
82
+ feature_list = [tracklet[f]['feat'] for f in frame_list]
83
+ io_time = [
84
+ cur_bias + frame_list[0] / 10., cur_bias + frame_list[-1] / 10.
85
+ ]
86
+ all_feat = np.array([feat for feat in feature_list])
87
+ mean_feat = np.mean(all_feat, axis=0)
88
+ tid_data[tid] = {
89
+ 'cam': cid,
90
+ 'tid': tid,
91
+ 'mean_feat': mean_feat,
92
+ 'zone_list': zone_list,
93
+ 'frame_list': frame_list,
94
+ 'tracklet': tracklet,
95
+ 'io_time': io_time
96
+ }
97
+ return tid_data, mot_list_break
98
+
99
+
100
+ def sub_cluster(cid_tid_dict,
101
+ scene_cluster,
102
+ use_ff=True,
103
+ use_rerank=True,
104
+ use_camera=False,
105
+ use_st_filter=False):
106
+ '''
107
+ cid_tid_dict: all camera_id and track_id
108
+ scene_cluster: like [41, 42, 43, 44, 45, 46] in AIC21 MTMCT S06 test videos
109
+ '''
110
+ assert (len(scene_cluster) != 0), "Error: scene_cluster length equals 0"
111
+ cid_tids = sorted(
112
+ [key for key in cid_tid_dict.keys() if key[0] in scene_cluster])
113
+ if use_camera:
114
+ clu = get_labels_with_camera(
115
+ cid_tid_dict,
116
+ cid_tids,
117
+ use_ff=use_ff,
118
+ use_rerank=use_rerank,
119
+ use_st_filter=use_st_filter)
120
+ else:
121
+ clu = get_labels(
122
+ cid_tid_dict,
123
+ cid_tids,
124
+ use_ff=use_ff,
125
+ use_rerank=use_rerank,
126
+ use_st_filter=use_st_filter)
127
+ new_clu = list()
128
+ for c_list in clu:
129
+ if len(c_list) <= 1: continue
130
+ cam_list = [cid_tids[c][0] for c in c_list]
131
+ if len(cam_list) != len(set(cam_list)): continue
132
+ new_clu.append([cid_tids[c] for c in c_list])
133
+ all_clu = new_clu
134
+ cid_tid_label = dict()
135
+ for i, c_list in enumerate(all_clu):
136
+ for c in c_list:
137
+ cid_tid_label[c] = i + 1
138
+ return cid_tid_label
139
+
140
+
141
+ def gen_res(output_dir_filename,
142
+ scene_cluster,
143
+ map_tid,
144
+ mot_list_breaks,
145
+ use_roi=False,
146
+ roi_dir=''):
147
+ f_w = open(output_dir_filename, 'w')
148
+ for idx, mot_feature in enumerate(mot_list_breaks):
149
+ cid = scene_cluster[idx]
150
+ img_rects = parse_pt_gt(mot_feature)
151
+ if use_roi:
152
+ assert (roi_dir != ''), "Error: roi_dir is not empty!"
153
+ roi = cv2.imread(os.path.join(roi_dir, f'c{cid:03d}/roi.jpg'), 0)
154
+ height, width = roi.shape
155
+
156
+ for fid in img_rects:
157
+ tid_rects = img_rects[fid]
158
+ fid = int(fid) + 1
159
+ for tid_rect in tid_rects:
160
+ tid = tid_rect[0]
161
+ rect = tid_rect[1:]
162
+ cx = 0.5 * rect[0] + 0.5 * rect[2]
163
+ cy = 0.5 * rect[1] + 0.5 * rect[3]
164
+ w = rect[2] - rect[0]
165
+ w = min(w * 1.2, w + 40)
166
+ h = rect[3] - rect[1]
167
+ h = min(h * 1.2, h + 40)
168
+ rect[2] -= rect[0]
169
+ rect[3] -= rect[1]
170
+ rect[0] = max(0, rect[0])
171
+ rect[1] = max(0, rect[1])
172
+ x1, y1 = max(0, cx - 0.5 * w), max(0, cy - 0.5 * h)
173
+ if use_roi:
174
+ x2, y2 = min(width, cx + 0.5 * w), min(height,
175
+ cy + 0.5 * h)
176
+ else:
177
+ x2, y2 = cx + 0.5 * w, cy + 0.5 * h
178
+ w, h = x2 - x1, y2 - y1
179
+ new_rect = list(map(int, [x1, y1, w, h]))
180
+ rect = list(map(int, rect))
181
+ if (cid, tid) in map_tid:
182
+ new_tid = map_tid[(cid, tid)]
183
+ f_w.write(
184
+ str(cid) + ' ' + str(new_tid) + ' ' + str(fid) + ' ' +
185
+ ' '.join(map(str, new_rect)) + ' -1 -1'
186
+ '\n')
187
+ print('gen_res: write file in {}'.format(output_dir_filename))
188
+ f_w.close()
189
+
190
+
191
+ def print_mtmct_result(gt_file, pred_file):
192
+ names = [
193
+ 'CameraId', 'Id', 'FrameId', 'X', 'Y', 'Width', 'Height', 'Xworld',
194
+ 'Yworld'
195
+ ]
196
+ gt = getData(gt_file, names=names)
197
+ pred = getData(pred_file, names=names)
198
+ summary = compare_dataframes_mtmc(gt, pred)
199
+ print('MTMCT summary: ', summary.columns.tolist())
200
+
201
+ formatters = {
202
+ 'idf1': '{:2.2f}'.format,
203
+ 'idp': '{:2.2f}'.format,
204
+ 'idr': '{:2.2f}'.format,
205
+ 'mota': '{:2.2f}'.format
206
+ }
207
+ summary = summary[['idf1', 'idp', 'idr', 'mota']]
208
+ summary.loc[:, 'idp'] *= 100
209
+ summary.loc[:, 'idr'] *= 100
210
+ summary.loc[:, 'idf1'] *= 100
211
+ summary.loc[:, 'mota'] *= 100
212
+ try:
213
+ import motmetrics as mm
214
+ except Exception as e:
215
+ raise RuntimeError(
216
+ 'Unable to use motmetrics in MTMCT in PP-Tracking, please install motmetrics, for example: `pip install motmetrics`, see https://github.com/longcw/py-motmetrics'
217
+ )
218
+ print(
219
+ mm.io.render_summary(
220
+ summary,
221
+ formatters=formatters,
222
+ namemap=mm.io.motchallenge_metric_names))
223
+
224
+
225
+ def get_mtmct_matching_results(pred_mtmct_file,
226
+ secs_interval=0.5,
227
+ video_fps=20):
228
+ res = np.loadtxt(pred_mtmct_file) # 'cid, tid, fid, x1, y1, w, h, -1, -1'
229
+ camera_ids = list(map(int, np.unique(res[:, 0])))
230
+
231
+ res = res[:, :7]
232
+ # each line in res: 'cid, tid, fid, x1, y1, w, h'
233
+
234
+ camera_tids = []
235
+ camera_results = dict()
236
+ for c_id in camera_ids:
237
+ camera_results[c_id] = res[res[:, 0] == c_id]
238
+ tids = np.unique(camera_results[c_id][:, 1])
239
+ tids = list(map(int, tids))
240
+ camera_tids.append(tids)
241
+
242
+ # select common tids throughout each video
243
+ common_tids = reduce(np.intersect1d, camera_tids)
244
+ if len(common_tids) == 0:
245
+ print(
246
+ 'No common tracked ids in these videos, please check your MOT result or select new videos.'
247
+ )
248
+ return None, None
249
+
250
+ # get mtmct matching results by cid_tid_fid_results[c_id][t_id][f_id]
251
+ cid_tid_fid_results = dict()
252
+ cid_tid_to_fids = dict()
253
+ interval = int(secs_interval * video_fps) # preferably less than 10
254
+ for c_id in camera_ids:
255
+ cid_tid_fid_results[c_id] = dict()
256
+ cid_tid_to_fids[c_id] = dict()
257
+ for t_id in common_tids:
258
+ tid_mask = camera_results[c_id][:, 1] == t_id
259
+ cid_tid_fid_results[c_id][t_id] = dict()
260
+
261
+ camera_trackid_results = camera_results[c_id][tid_mask]
262
+ fids = np.unique(camera_trackid_results[:, 2])
263
+ fids = fids[fids % interval == 0]
264
+ fids = list(map(int, fids))
265
+ cid_tid_to_fids[c_id][t_id] = fids
266
+
267
+ for f_id in fids:
268
+ st_frame = f_id
269
+ ed_frame = f_id + interval
270
+
271
+ st_mask = camera_trackid_results[:, 2] >= st_frame
272
+ ed_mask = camera_trackid_results[:, 2] < ed_frame
273
+ frame_mask = np.logical_and(st_mask, ed_mask)
274
+ cid_tid_fid_results[c_id][t_id][f_id] = camera_trackid_results[
275
+ frame_mask]
276
+
277
+ return camera_results, cid_tid_fid_results
278
+
279
+
280
+ def save_mtmct_crops(cid_tid_fid_res,
281
+ images_dir,
282
+ crops_dir,
283
+ width=300,
284
+ height=200):
285
+ camera_ids = cid_tid_fid_res.keys()
286
+ seqs_folder = os.listdir(images_dir)
287
+ seqs = []
288
+ for x in seqs_folder:
289
+ if os.path.isdir(os.path.join(images_dir, x)):
290
+ seqs.append(x)
291
+ assert len(seqs) == len(camera_ids)
292
+ seqs.sort()
293
+
294
+ if not os.path.exists(crops_dir):
295
+ os.makedirs(crops_dir)
296
+
297
+ common_tids = list(cid_tid_fid_res[list(camera_ids)[0]].keys())
298
+
299
+ # get crops by name 'tid_cid_fid.jpg
300
+ for t_id in common_tids:
301
+ for i, c_id in enumerate(camera_ids):
302
+ infer_dir = os.path.join(images_dir, seqs[i])
303
+ if os.path.exists(os.path.join(infer_dir, 'img1')):
304
+ infer_dir = os.path.join(infer_dir, 'img1')
305
+ all_images = os.listdir(infer_dir)
306
+ all_images.sort()
307
+
308
+ for f_id in cid_tid_fid_res[c_id][t_id].keys():
309
+ frame_idx = f_id - 1 if f_id > 0 else 0
310
+ im_path = os.path.join(infer_dir, all_images[frame_idx])
311
+
312
+ im = cv2.imread(im_path) # (H, W, 3)
313
+
314
+ # only select one track
315
+ track = cid_tid_fid_res[c_id][t_id][f_id][0]
316
+
317
+ cid, tid, fid, x1, y1, w, h = [int(v) for v in track]
318
+ clip = im[y1:(y1 + h), x1:(x1 + w)]
319
+ clip = cv2.resize(clip, (width, height))
320
+
321
+ cv2.imwrite(
322
+ os.path.join(crops_dir,
323
+ 'tid{:06d}_cid{:06d}_fid{:06d}.jpg'.format(
324
+ tid, cid, fid)), clip)
325
+
326
+ print("Finish cropping image of tracked_id {} in camera: {}".
327
+ format(t_id, c_id))
328
+
329
+
330
+ def save_mtmct_vis_results(camera_results,
331
+ images_dir,
332
+ save_dir,
333
+ save_videos=False):
334
+ # camera_results: 'cid, tid, fid, x1, y1, w, h'
335
+ camera_ids = camera_results.keys()
336
+ seqs_folder = os.listdir(images_dir)
337
+ seqs = []
338
+ for x in seqs_folder:
339
+ if os.path.isdir(os.path.join(images_dir, x)):
340
+ seqs.append(x)
341
+ assert len(seqs) == len(camera_ids)
342
+ seqs.sort()
343
+
344
+ if not os.path.exists(save_dir):
345
+ os.makedirs(save_dir)
346
+
347
+ for i, c_id in enumerate(camera_ids):
348
+ print("Start visualization for camera {} of sequence {}.".format(
349
+ c_id, seqs[i]))
350
+ cid_save_dir = os.path.join(save_dir, '{}'.format(seqs[i]))
351
+ if not os.path.exists(cid_save_dir):
352
+ os.makedirs(cid_save_dir)
353
+
354
+ infer_dir = os.path.join(images_dir, seqs[i])
355
+ if os.path.exists(os.path.join(infer_dir, 'img1')):
356
+ infer_dir = os.path.join(infer_dir, 'img1')
357
+ all_images = os.listdir(infer_dir)
358
+ all_images.sort()
359
+
360
+ for f_id, im_path in enumerate(all_images):
361
+ img = cv2.imread(os.path.join(infer_dir, im_path))
362
+ tracks = camera_results[c_id][camera_results[c_id][:, 2] == f_id]
363
+ if tracks.shape[0] > 0:
364
+ tracked_ids = tracks[:, 1]
365
+ xywhs = tracks[:, 3:]
366
+ online_im = plot_tracking(
367
+ img, xywhs, tracked_ids, scores=None, frame_id=f_id)
368
+ else:
369
+ online_im = img
370
+ print('Frame {} of seq {} has no tracking results'.format(
371
+ f_id, seqs[i]))
372
+
373
+ cv2.imwrite(
374
+ os.path.join(cid_save_dir, '{:05d}.jpg'.format(f_id)),
375
+ online_im)
376
+ if f_id % 40 == 0:
377
+ print('Processing frame {}'.format(f_id))
378
+
379
+ if save_videos:
380
+ output_video_path = os.path.join(
381
+ cid_save_dir, '..', '{}_mtmct_vis.mp4'.format(seqs[i]))
382
+ cmd_str = 'ffmpeg -f image2 -i {}/%05d.jpg {}'.format(
383
+ cid_save_dir, output_video_path)
384
+ os.system(cmd_str)
385
+ print('Save camera {} video in {}.'.format(seqs[i],
386
+ output_video_path))
pptracking/python/mot/mtmct/utils.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/LCFractal/AIC21-MTMC/tree/main/reid/reid-matching/tools
16
+ """
17
+
18
+ import os
19
+ import re
20
+ import cv2
21
+ import gc
22
+ import numpy as np
23
+ import pandas as pd
24
+ from tqdm import tqdm
25
+ import warnings
26
+ warnings.filterwarnings("ignore")
27
+
28
+ __all__ = [
29
+ 'parse_pt', 'parse_bias', 'get_dire', 'parse_pt_gt',
30
+ 'compare_dataframes_mtmc', 'get_sim_matrix', 'get_labels', 'getData',
31
+ 'gen_new_mot'
32
+ ]
33
+
34
+
35
+ def parse_pt(mot_feature, zones=None):
36
+ mot_list = dict()
37
+ for line in mot_feature:
38
+ fid = int(re.sub('[a-z,A-Z]', "", mot_feature[line]['frame']))
39
+ tid = mot_feature[line]['id']
40
+ bbox = list(map(lambda x: int(float(x)), mot_feature[line]['bbox']))
41
+ if tid not in mot_list:
42
+ mot_list[tid] = dict()
43
+ out_dict = mot_feature[line]
44
+ if zones is not None:
45
+ out_dict['zone'] = zones.get_zone(bbox)
46
+ else:
47
+ out_dict['zone'] = None
48
+ mot_list[tid][fid] = out_dict
49
+ return mot_list
50
+
51
+
52
+ def gen_new_mot(mot_list):
53
+ out_dict = dict()
54
+ for tracklet in mot_list:
55
+ tracklet = mot_list[tracklet]
56
+ for f in tracklet:
57
+ out_dict[tracklet[f]['imgname']] = tracklet[f]
58
+ return out_dict
59
+
60
+
61
+ def mergesetfeat1_notrk(P, neg_vector, in_feats, in_labels):
62
+ out_feats = []
63
+ for i in range(in_feats.shape[0]):
64
+ camera_id = in_labels[i, 1]
65
+ feat = in_feats[i] - neg_vector[camera_id]
66
+ feat = P[camera_id].dot(feat)
67
+ feat = feat / np.linalg.norm(feat, ord=2)
68
+ out_feats.append(feat)
69
+ out_feats = np.vstack(out_feats)
70
+ return out_feats
71
+
72
+
73
+ def compute_P2(prb_feats, gal_feats, gal_labels, la=3.0):
74
+ X = gal_feats
75
+ neg_vector = {}
76
+ u_labels = np.unique(gal_labels[:, 1])
77
+ P = {}
78
+ for label in u_labels:
79
+ curX = gal_feats[gal_labels[:, 1] == label, :]
80
+ neg_vector[label] = np.mean(curX, axis=0)
81
+ P[label] = np.linalg.inv(
82
+ curX.T.dot(curX) + curX.shape[0] * la * np.eye(X.shape[1]))
83
+ return P, neg_vector
84
+
85
+
86
+ def parse_bias(cameras_bias):
87
+ cid_bias = dict()
88
+ for cameras in cameras_bias.keys():
89
+ cameras_id = re.sub('[a-z,A-Z]', "", cameras)
90
+ cameras_id = int(cameras_id)
91
+ bias = cameras_bias[cameras]
92
+ cid_bias[cameras_id] = float(bias)
93
+ return cid_bias
94
+
95
+
96
+ def get_dire(zone_list, cid):
97
+ zs, ze = zone_list[0], zone_list[-1]
98
+ return (zs, ze)
99
+
100
+
101
+ def intracam_ignore(st_mask, cid_tids):
102
+ count = len(cid_tids)
103
+ for i in range(count):
104
+ for j in range(count):
105
+ if cid_tids[i][0] == cid_tids[j][0]:
106
+ st_mask[i, j] = 0.
107
+ return st_mask
108
+
109
+
110
+ def mergesetfeat(in_feats, in_labels, in_tracks):
111
+ trackset = list(set(list(in_tracks)))
112
+ out_feats = []
113
+ out_labels = []
114
+ for track in trackset:
115
+ feat = np.mean(in_feats[in_tracks == track], axis=0)
116
+ feat = feat / np.linalg.norm(feat, ord=2)
117
+ label = in_labels[in_tracks == track][0]
118
+ out_feats.append(feat)
119
+ out_labels.append(label)
120
+ out_feats = np.vstack(out_feats)
121
+ out_labels = np.vstack(out_labels)
122
+ return out_feats, out_labels
123
+
124
+
125
+ def mergesetfeat3(X, labels, gX, glabels, beta=0.08, knn=20, lr=0.5):
126
+ for i in range(0, X.shape[0]):
127
+ if i % 1000 == 0:
128
+ print('feat3:%d/%d' % (i, X.shape[0]))
129
+ knnX = gX[glabels[:, 1] != labels[i, 1], :]
130
+ sim = knnX.dot(X[i, :])
131
+ knnX = knnX[sim > 0, :]
132
+ sim = sim[sim > 0]
133
+ if len(sim) > 0:
134
+ idx = np.argsort(-sim)
135
+ if len(sim) > 2 * knn:
136
+ sim = sim[idx[:2 * knn]]
137
+ knnX = knnX[idx[:2 * knn], :]
138
+ else:
139
+ sim = sim[idx]
140
+ knnX = knnX[idx, :]
141
+ knn = min(knn, len(sim))
142
+ knn_pos_weight = np.exp((sim[:knn] - 1) / beta)
143
+ knn_neg_weight = np.ones(len(sim) - knn)
144
+ knn_pos_prob = knn_pos_weight / np.sum(knn_pos_weight)
145
+ knn_neg_prob = knn_neg_weight / np.sum(knn_neg_weight)
146
+ X[i, :] += lr * (knn_pos_prob.dot(knnX[:knn, :]) -
147
+ knn_neg_prob.dot(knnX[knn:, :]))
148
+ X[i, :] /= np.linalg.norm(X[i, :])
149
+ return X
150
+
151
+
152
+ def run_fic(prb_feats, gal_feats, prb_labels, gal_labels, la=3.0):
153
+ P, neg_vector = compute_P2(prb_feats, gal_feats, gal_labels, la)
154
+ prb_feats_new = mergesetfeat1_notrk(P, neg_vector, prb_feats, prb_labels)
155
+ gal_feats_new = mergesetfeat1_notrk(P, neg_vector, gal_feats, gal_labels)
156
+ return prb_feats_new, gal_feats_new
157
+
158
+
159
+ def run_fac(prb_feats,
160
+ gal_feats,
161
+ prb_labels,
162
+ gal_labels,
163
+ beta=0.08,
164
+ knn=20,
165
+ lr=0.5,
166
+ prb_epoch=2,
167
+ gal_epoch=3):
168
+ gal_feats_new = gal_feats.copy()
169
+ for i in range(prb_epoch):
170
+ gal_feats_new = mergesetfeat3(gal_feats_new, gal_labels, gal_feats,
171
+ gal_labels, beta, knn, lr)
172
+ prb_feats_new = prb_feats.copy()
173
+ for i in range(gal_epoch):
174
+ prb_feats_new = mergesetfeat3(prb_feats_new, prb_labels, gal_feats_new,
175
+ gal_labels, beta, knn, lr)
176
+ return prb_feats_new, gal_feats_new
177
+
178
+
179
+ def euclidean_distance(qf, gf):
180
+ m = qf.shape[0]
181
+ n = gf.shape[0]
182
+ dist_mat = 2 - 2 * np.matmul(qf, gf.T)
183
+ return dist_mat
184
+
185
+
186
+ def find_topk(a, k, axis=-1, largest=True, sorted=True):
187
+ if axis is None:
188
+ axis_size = a.size
189
+ else:
190
+ axis_size = a.shape[axis]
191
+ assert 1 <= k <= axis_size
192
+
193
+ a = np.asanyarray(a)
194
+ if largest:
195
+ index_array = np.argpartition(a, axis_size - k, axis=axis)
196
+ topk_indices = np.take(index_array, -np.arange(k) - 1, axis=axis)
197
+ else:
198
+ index_array = np.argpartition(a, k - 1, axis=axis)
199
+ topk_indices = np.take(index_array, np.arange(k), axis=axis)
200
+ topk_values = np.take_along_axis(a, topk_indices, axis=axis)
201
+ if sorted:
202
+ sorted_indices_in_topk = np.argsort(topk_values, axis=axis)
203
+ if largest:
204
+ sorted_indices_in_topk = np.flip(sorted_indices_in_topk, axis=axis)
205
+ sorted_topk_values = np.take_along_axis(
206
+ topk_values, sorted_indices_in_topk, axis=axis)
207
+ sorted_topk_indices = np.take_along_axis(
208
+ topk_indices, sorted_indices_in_topk, axis=axis)
209
+ return sorted_topk_values, sorted_topk_indices
210
+ return topk_values, topk_indices
211
+
212
+
213
+ def batch_numpy_topk(qf, gf, k1, N=6000):
214
+ m = qf.shape[0]
215
+ n = gf.shape[0]
216
+ initial_rank = []
217
+ for j in range(n // N + 1):
218
+ temp_gf = gf[j * N:j * N + N]
219
+ temp_qd = []
220
+ for i in range(m // N + 1):
221
+ temp_qf = qf[i * N:i * N + N]
222
+ temp_d = euclidean_distance(temp_qf, temp_gf)
223
+ temp_qd.append(temp_d)
224
+ temp_qd = np.concatenate(temp_qd, axis=0)
225
+ temp_qd = temp_qd / (np.max(temp_qd, axis=0)[0])
226
+ temp_qd = temp_qd.T
227
+ initial_rank.append(
228
+ find_topk(
229
+ temp_qd, k=k1, axis=1, largest=False, sorted=True)[1])
230
+ del temp_qd
231
+ del temp_gf
232
+ del temp_qf
233
+ del temp_d
234
+ initial_rank = np.concatenate(initial_rank, axis=0)
235
+ return initial_rank
236
+
237
+
238
+ def batch_euclidean_distance(qf, gf, N=6000):
239
+ m = qf.shape[0]
240
+ n = gf.shape[0]
241
+ dist_mat = []
242
+ for j in range(n // N + 1):
243
+ temp_gf = gf[j * N:j * N + N]
244
+ temp_qd = []
245
+ for i in range(m // N + 1):
246
+ temp_qf = qf[i * N:i * N + N]
247
+ temp_d = euclidean_distance(temp_qf, temp_gf)
248
+ temp_qd.append(temp_d)
249
+ temp_qd = np.concatenate(temp_qd, axis=0)
250
+ temp_qd = temp_qd / (np.max(temp_qd, axis=0)[0])
251
+ dist_mat.append(temp_qd.T)
252
+ del temp_qd
253
+ del temp_gf
254
+ del temp_qf
255
+ del temp_d
256
+ dist_mat = np.concatenate(dist_mat, axis=0)
257
+ return dist_mat
258
+
259
+
260
+ def batch_v(feat, R, all_num):
261
+ V = np.zeros((all_num, all_num), dtype=np.float32)
262
+ m = feat.shape[0]
263
+ for i in tqdm(range(m)):
264
+ temp_gf = feat[i].reshape(1, -1)
265
+ temp_qd = euclidean_distance(temp_gf, feat)
266
+ temp_qd = temp_qd / (np.max(temp_qd))
267
+ temp_qd = temp_qd.reshape(-1)
268
+ temp_qd = temp_qd[R[i].tolist()]
269
+ weight = np.exp(-temp_qd)
270
+ weight = weight / np.sum(weight)
271
+ V[i, R[i]] = weight.astype(np.float32)
272
+ return V
273
+
274
+
275
+ def k_reciprocal_neigh(initial_rank, i, k1):
276
+ forward_k_neigh_index = initial_rank[i, :k1 + 1]
277
+ backward_k_neigh_index = initial_rank[forward_k_neigh_index, :k1 + 1]
278
+ fi = np.where(backward_k_neigh_index == i)[0]
279
+ return forward_k_neigh_index[fi]
280
+
281
+
282
+ def ReRank2(probFea, galFea, k1=20, k2=6, lambda_value=0.3):
283
+ query_num = probFea.shape[0]
284
+ all_num = query_num + galFea.shape[0]
285
+ feat = np.concatenate((probFea, galFea), axis=0)
286
+
287
+ initial_rank = batch_numpy_topk(feat, feat, k1 + 1, N=6000)
288
+ del probFea
289
+ del galFea
290
+ gc.collect() # empty memory
291
+ R = []
292
+ for i in tqdm(range(all_num)):
293
+ # k-reciprocal neighbors
294
+ k_reciprocal_index = k_reciprocal_neigh(initial_rank, i, k1)
295
+ k_reciprocal_expansion_index = k_reciprocal_index
296
+ for j in range(len(k_reciprocal_index)):
297
+ candidate = k_reciprocal_index[j]
298
+ candidate_k_reciprocal_index = k_reciprocal_neigh(
299
+ initial_rank, candidate, int(np.around(k1 / 2)))
300
+ if len(
301
+ np.intersect1d(candidate_k_reciprocal_index,
302
+ k_reciprocal_index)) > 2. / 3 * len(
303
+ candidate_k_reciprocal_index):
304
+ k_reciprocal_expansion_index = np.append(
305
+ k_reciprocal_expansion_index, candidate_k_reciprocal_index)
306
+ k_reciprocal_expansion_index = np.unique(k_reciprocal_expansion_index)
307
+ R.append(k_reciprocal_expansion_index)
308
+
309
+ gc.collect() # empty memory
310
+ V = batch_v(feat, R, all_num)
311
+ del R
312
+ gc.collect() # empty memory
313
+ initial_rank = initial_rank[:, :k2]
314
+
315
+ # Faster version
316
+ if k2 != 1:
317
+ V_qe = np.zeros_like(V, dtype=np.float16)
318
+ for i in range(all_num):
319
+ V_qe[i, :] = np.mean(V[initial_rank[i], :], axis=0)
320
+ V = V_qe
321
+ del V_qe
322
+ del initial_rank
323
+ gc.collect() # empty memory
324
+ invIndex = []
325
+ for i in range(all_num):
326
+ invIndex.append(np.where(V[:, i] != 0)[0])
327
+ jaccard_dist = np.zeros((query_num, all_num), dtype=np.float32)
328
+ for i in tqdm(range(query_num)):
329
+ temp_min = np.zeros(shape=[1, all_num], dtype=np.float32)
330
+ indNonZero = np.where(V[i, :] != 0)[0]
331
+ indImages = [invIndex[ind] for ind in indNonZero]
332
+ for j in range(len(indNonZero)):
333
+ temp_min[0, indImages[j]] = temp_min[0, indImages[j]] + np.minimum(
334
+ V[i, indNonZero[j]], V[indImages[j], indNonZero[j]])
335
+ jaccard_dist[i] = 1 - temp_min / (2. - temp_min)
336
+ del V
337
+ gc.collect() # empty memory
338
+ original_dist = batch_euclidean_distance(feat, feat[:query_num, :])
339
+ final_dist = jaccard_dist * (1 - lambda_value
340
+ ) + original_dist * lambda_value
341
+ del original_dist
342
+ del jaccard_dist
343
+ final_dist = final_dist[:query_num, query_num:]
344
+ return final_dist
345
+
346
+
347
+ def visual_rerank(prb_feats,
348
+ gal_feats,
349
+ cid_tids,
350
+ use_ff=False,
351
+ use_rerank=False):
352
+ """Rerank by visual cures."""
353
+ gal_labels = np.array([[0, item[0]] for item in cid_tids])
354
+ prb_labels = gal_labels.copy()
355
+ if use_ff:
356
+ print('current use ff finetuned parameters....')
357
+ # Step1-1: fic. finetuned parameters: [la]
358
+ prb_feats, gal_feats = run_fic(prb_feats, gal_feats, prb_labels,
359
+ gal_labels, 3.0)
360
+ # Step1=2: fac. finetuned parameters: [beta,knn,lr,prb_epoch,gal_epoch]
361
+ prb_feats, gal_feats = run_fac(prb_feats, gal_feats, prb_labels,
362
+ gal_labels, 0.08, 20, 0.5, 1, 1)
363
+ if use_rerank:
364
+ print('current use rerank finetuned parameters....')
365
+ # Step2: k-reciprocal. finetuned parameters: [k1,k2,lambda_value]
366
+ sims = ReRank2(prb_feats, gal_feats, 20, 3, 0.3)
367
+ else:
368
+ sims = 1.0 - np.dot(prb_feats, gal_feats.T)
369
+
370
+ # NOTE: sims here is actually dist, the smaller the more similar
371
+ return 1.0 - sims
372
+
373
+
374
+ def normalize(nparray, axis=0):
375
+ try:
376
+ from sklearn import preprocessing
377
+ except Exception as e:
378
+ raise RuntimeError(
379
+ 'Unable to use sklearn in MTMCT in PP-Tracking, please install sklearn, for example: `pip install sklearn`'
380
+ )
381
+ nparray = preprocessing.normalize(nparray, norm='l2', axis=axis)
382
+ return nparray
383
+
384
+
385
+ def get_match(cluster_labels):
386
+ cluster_dict = dict()
387
+ cluster = list()
388
+ for i, l in enumerate(cluster_labels):
389
+ if l in list(cluster_dict.keys()):
390
+ cluster_dict[l].append(i)
391
+ else:
392
+ cluster_dict[l] = [i]
393
+ for idx in cluster_dict:
394
+ cluster.append(cluster_dict[idx])
395
+ return cluster
396
+
397
+
398
+ def get_cid_tid(cluster_labels, cid_tids):
399
+ cluster = list()
400
+ for labels in cluster_labels:
401
+ cid_tid_list = list()
402
+ for label in labels:
403
+ cid_tid_list.append(cid_tids[label])
404
+ cluster.append(cid_tid_list)
405
+ return cluster
406
+
407
+
408
+ def combin_feature(cid_tid_dict, sub_cluster):
409
+ for sub_ct in sub_cluster:
410
+ if len(sub_ct) < 2: continue
411
+ mean_feat = np.array([cid_tid_dict[i]['mean_feat'] for i in sub_ct])
412
+ for i in sub_ct:
413
+ cid_tid_dict[i]['mean_feat'] = mean_feat.mean(axis=0)
414
+ return cid_tid_dict
415
+
416
+
417
+ def combin_cluster(sub_labels, cid_tids):
418
+ cluster = list()
419
+ for sub_c_to_c in sub_labels:
420
+ if len(cluster) < 1:
421
+ cluster = sub_labels[sub_c_to_c]
422
+ continue
423
+ for c_ts in sub_labels[sub_c_to_c]:
424
+ is_add = False
425
+ for i_c, c_set in enumerate(cluster):
426
+ if len(set(c_ts) & set(c_set)) > 0:
427
+ new_list = list(set(c_ts) | set(c_set))
428
+ cluster[i_c] = new_list
429
+ is_add = True
430
+ break
431
+ if not is_add:
432
+ cluster.append(c_ts)
433
+ labels = list()
434
+ num_tr = 0
435
+ for c_ts in cluster:
436
+ label_list = list()
437
+ for c_t in c_ts:
438
+ label_list.append(cid_tids.index(c_t))
439
+ num_tr += 1
440
+ label_list.sort()
441
+ labels.append(label_list)
442
+ return labels, cluster
443
+
444
+
445
+ def parse_pt_gt(mot_feature):
446
+ img_rects = dict()
447
+ for line in mot_feature:
448
+ fid = int(re.sub('[a-z,A-Z]', "", mot_feature[line]['frame']))
449
+ tid = mot_feature[line]['id']
450
+ rect = list(map(lambda x: int(float(x)), mot_feature[line]['bbox']))
451
+ if fid not in img_rects:
452
+ img_rects[fid] = list()
453
+ rect.insert(0, tid)
454
+ img_rects[fid].append(rect)
455
+ return img_rects
456
+
457
+
458
+ # eval result
459
+ def compare_dataframes_mtmc(gts, ts):
460
+ try:
461
+ import motmetrics as mm
462
+ except Exception as e:
463
+ raise RuntimeError(
464
+ 'Unable to use motmetrics in MTMCT in PP-Tracking, please install motmetrics, for example: `pip install motmetrics`, see https://github.com/longcw/py-motmetrics'
465
+ )
466
+ """Compute ID-based evaluation metrics for MTMCT
467
+ Return:
468
+ df (pandas.DataFrame): Results of the evaluations in a df with only the 'idf1', 'idp', and 'idr' columns.
469
+ """
470
+ gtds = []
471
+ tsds = []
472
+ gtcams = gts['CameraId'].drop_duplicates().tolist()
473
+ tscams = ts['CameraId'].drop_duplicates().tolist()
474
+ maxFrameId = 0
475
+
476
+ for k in sorted(gtcams):
477
+ gtd = gts.query('CameraId == %d' % k)
478
+ gtd = gtd[['FrameId', 'Id', 'X', 'Y', 'Width', 'Height']]
479
+ # max FrameId in gtd only
480
+ mfid = gtd['FrameId'].max()
481
+ gtd['FrameId'] += maxFrameId
482
+ gtd = gtd.set_index(['FrameId', 'Id'])
483
+ gtds.append(gtd)
484
+
485
+ if k in tscams:
486
+ tsd = ts.query('CameraId == %d' % k)
487
+ tsd = tsd[['FrameId', 'Id', 'X', 'Y', 'Width', 'Height']]
488
+ # max FrameId among both gtd and tsd
489
+ mfid = max(mfid, tsd['FrameId'].max())
490
+ tsd['FrameId'] += maxFrameId
491
+ tsd = tsd.set_index(['FrameId', 'Id'])
492
+ tsds.append(tsd)
493
+
494
+ maxFrameId += mfid
495
+
496
+ # compute multi-camera tracking evaluation stats
497
+ multiCamAcc = mm.utils.compare_to_groundtruth(
498
+ pd.concat(gtds), pd.concat(tsds), 'iou')
499
+ metrics = list(mm.metrics.motchallenge_metrics)
500
+ metrics.extend(['num_frames', 'idfp', 'idfn', 'idtp'])
501
+ mh = mm.metrics.create()
502
+ summary = mh.compute(multiCamAcc, metrics=metrics, name='MultiCam')
503
+ return summary
504
+
505
+
506
+ def get_sim_matrix(cid_tid_dict,
507
+ cid_tids,
508
+ use_ff=True,
509
+ use_rerank=True,
510
+ use_st_filter=False):
511
+ # Note: camera independent get_sim_matrix function,
512
+ # which is different from the one in camera_utils.py.
513
+ count = len(cid_tids)
514
+
515
+ q_arr = np.array(
516
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
517
+ g_arr = np.array(
518
+ [cid_tid_dict[cid_tids[i]]['mean_feat'] for i in range(count)])
519
+ q_arr = normalize(q_arr, axis=1)
520
+ g_arr = normalize(g_arr, axis=1)
521
+
522
+ st_mask = np.ones((count, count), dtype=np.float32)
523
+ st_mask = intracam_ignore(st_mask, cid_tids)
524
+
525
+ visual_sim_matrix = visual_rerank(
526
+ q_arr, g_arr, cid_tids, use_ff=use_ff, use_rerank=use_rerank)
527
+ visual_sim_matrix = visual_sim_matrix.astype('float32')
528
+
529
+ np.set_printoptions(precision=3)
530
+ sim_matrix = visual_sim_matrix * st_mask
531
+
532
+ np.fill_diagonal(sim_matrix, 0)
533
+ return sim_matrix
534
+
535
+
536
+ def get_labels(cid_tid_dict,
537
+ cid_tids,
538
+ use_ff=True,
539
+ use_rerank=True,
540
+ use_st_filter=False):
541
+ try:
542
+ from sklearn.cluster import AgglomerativeClustering
543
+ except Exception as e:
544
+ raise RuntimeError(
545
+ 'Unable to use sklearn in MTMCT in PP-Tracking, please install sklearn, for example: `pip install sklearn`'
546
+ )
547
+ # 1st cluster
548
+ sim_matrix = get_sim_matrix(
549
+ cid_tid_dict,
550
+ cid_tids,
551
+ use_ff=use_ff,
552
+ use_rerank=use_rerank,
553
+ use_st_filter=use_st_filter)
554
+ cluster_labels = AgglomerativeClustering(
555
+ n_clusters=None,
556
+ distance_threshold=0.5,
557
+ affinity='precomputed',
558
+ linkage='complete').fit_predict(1 - sim_matrix)
559
+ labels = get_match(cluster_labels)
560
+ sub_cluster = get_cid_tid(labels, cid_tids)
561
+
562
+ # 2nd cluster
563
+ cid_tid_dict_new = combin_feature(cid_tid_dict, sub_cluster)
564
+ sim_matrix = get_sim_matrix(
565
+ cid_tid_dict_new,
566
+ cid_tids,
567
+ use_ff=use_ff,
568
+ use_rerank=use_rerank,
569
+ use_st_filter=use_st_filter)
570
+ cluster_labels = AgglomerativeClustering(
571
+ n_clusters=None,
572
+ distance_threshold=0.9,
573
+ affinity='precomputed',
574
+ linkage='complete').fit_predict(1 - sim_matrix)
575
+ labels = get_match(cluster_labels)
576
+ sub_cluster = get_cid_tid(labels, cid_tids)
577
+
578
+ return labels
579
+
580
+
581
+ def getData(fpath, names=None, sep='\s+|\t+|,'):
582
+ """ Get the necessary track data from a file handle.
583
+ Args:
584
+ fpath (str) : Original path of file reading from.
585
+ names (list[str]): List of column names for the data.
586
+ sep (str): Allowed separators regular expression string.
587
+ Return:
588
+ df (pandas.DataFrame): Data frame containing the data loaded from the
589
+ stream with optionally assigned column names. No index is set on the data.
590
+ """
591
+ try:
592
+ df = pd.read_csv(
593
+ fpath,
594
+ sep=sep,
595
+ index_col=None,
596
+ skipinitialspace=True,
597
+ header=None,
598
+ names=names,
599
+ engine='python')
600
+ return df
601
+
602
+ except Exception as e:
603
+ raise ValueError("Could not read input from %s. Error: %s" %
604
+ (fpath, repr(e)))
pptracking/python/mot/mtmct/zone.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/LCFractal/AIC21-MTMC/tree/main/reid/reid-matching/tools
16
+
17
+ Note: The following codes are strongly related to zone of the AIC21 test-set S06,
18
+ so they can only be used in S06, and can not be used for other MTMCT datasets.
19
+ """
20
+
21
+ import os
22
+ import cv2
23
+ import numpy as np
24
+ try:
25
+ from sklearn.cluster import AgglomerativeClustering
26
+ except:
27
+ print(
28
+ 'Warning: Unable to use MTMCT in PP-Tracking, please install sklearn, for example: `pip install sklearn`'
29
+ )
30
+ pass
31
+
32
+ BBOX_B = 10 / 15
33
+
34
+
35
+ class Zone(object):
36
+ def __init__(self, zone_path='datasets/zone'):
37
+ # 0: b 1: g 3: r 123:w
38
+ # w r not high speed
39
+ # b g high speed
40
+ assert zone_path != '', "Error: zone_path is not empty!"
41
+ zones = {}
42
+ for img_name in os.listdir(zone_path):
43
+ camnum = int(img_name.split('.')[0][-3:])
44
+ zone_img = cv2.imread(os.path.join(zone_path, img_name))
45
+ zones[camnum] = zone_img
46
+ self.zones = zones
47
+ self.current_cam = 0
48
+
49
+ def set_cam(self, cam):
50
+ self.current_cam = cam
51
+
52
+ def get_zone(self, bbox):
53
+ cx = int((bbox[0] + bbox[2]) / 2)
54
+ cy = int((bbox[1] + bbox[3]) / 2)
55
+ pix = self.zones[self.current_cam][max(cy - 1, 0), max(cx - 1, 0), :]
56
+ zone_num = 0
57
+ if pix[0] > 50 and pix[1] > 50 and pix[2] > 50: # w
58
+ zone_num = 1
59
+ if pix[0] < 50 and pix[1] < 50 and pix[2] > 50: # r
60
+ zone_num = 2
61
+ if pix[0] < 50 and pix[1] > 50 and pix[2] < 50: # g
62
+ zone_num = 3
63
+ if pix[0] > 50 and pix[1] < 50 and pix[2] < 50: # b
64
+ zone_num = 4
65
+ return zone_num
66
+
67
+ def is_ignore(self, zone_list, frame_list, cid):
68
+ # 0 not in any corssroad, 1 white 2 red 3 green 4 bule
69
+ zs, ze = zone_list[0], zone_list[-1]
70
+ fs, fe = frame_list[0], frame_list[-1]
71
+ if zs == ze:
72
+ # if always on one section, excluding
73
+ if ze in [1, 2]:
74
+ return 2
75
+ if zs != 0 and 0 in zone_list:
76
+ return 0
77
+ if fe - fs > 1500:
78
+ return 2
79
+ if fs < 2:
80
+ if cid in [45]:
81
+ if ze in [3, 4]:
82
+ return 1
83
+ else:
84
+ return 2
85
+ if fe > 1999:
86
+ if cid in [41]:
87
+ if ze not in [3]:
88
+ return 2
89
+ else:
90
+ return 0
91
+ if fs < 2 or fe > 1999:
92
+ if ze in [3, 4]:
93
+ return 0
94
+ if ze in [3, 4]:
95
+ return 1
96
+ return 2
97
+ else:
98
+ # if camera section change
99
+ if cid in [41, 42, 43, 44, 45, 46]:
100
+ # come from road extension, exclusing
101
+ if zs == 1 and ze == 2:
102
+ return 2
103
+ if zs == 2 and ze == 1:
104
+ return 2
105
+ if cid in [41]:
106
+ # On 41 camera, no vehicle come into 42 camera
107
+ if (zs in [1, 2]) and ze == 4:
108
+ return 2
109
+ if zs == 4 and (ze in [1, 2]):
110
+ return 2
111
+ if cid in [46]:
112
+ # On 46 camera,no vehicle come into 45
113
+ if (zs in [1, 2]) and ze == 3:
114
+ return 2
115
+ if zs == 3 and (ze in [1, 2]):
116
+ return 2
117
+ return 0
118
+
119
+ def filter_mot(self, mot_list, cid):
120
+ new_mot_list = dict()
121
+ sub_mot_list = dict()
122
+ for tracklet in mot_list:
123
+ tracklet_dict = mot_list[tracklet]
124
+ frame_list = list(tracklet_dict.keys())
125
+ frame_list.sort()
126
+ zone_list = []
127
+ for f in frame_list:
128
+ zone_list.append(tracklet_dict[f]['zone'])
129
+ if self.is_ignore(zone_list, frame_list, cid) == 0:
130
+ new_mot_list[tracklet] = tracklet_dict
131
+ if self.is_ignore(zone_list, frame_list, cid) == 1:
132
+ sub_mot_list[tracklet] = tracklet_dict
133
+ return new_mot_list
134
+
135
+ def filter_bbox(self, mot_list, cid):
136
+ new_mot_list = dict()
137
+ yh = self.zones[cid].shape[0]
138
+ for tracklet in mot_list:
139
+ tracklet_dict = mot_list[tracklet]
140
+ frame_list = list(tracklet_dict.keys())
141
+ frame_list.sort()
142
+ bbox_list = []
143
+ for f in frame_list:
144
+ bbox_list.append(tracklet_dict[f]['bbox'])
145
+ bbox_x = [b[0] for b in bbox_list]
146
+ bbox_y = [b[1] for b in bbox_list]
147
+ bbox_w = [b[2] - b[0] for b in bbox_list]
148
+ bbox_h = [b[3] - b[1] for b in bbox_list]
149
+ new_frame_list = list()
150
+ if 0 in bbox_x or 0 in bbox_y:
151
+ b0 = [
152
+ i for i, f in enumerate(frame_list)
153
+ if bbox_x[i] < 5 or bbox_y[i] + bbox_h[i] > yh - 5
154
+ ]
155
+ if len(b0) == len(frame_list):
156
+ if cid in [41, 42, 44, 45, 46]:
157
+ continue
158
+ max_w = max(bbox_w)
159
+ max_h = max(bbox_h)
160
+ for i, f in enumerate(frame_list):
161
+ if bbox_w[i] > max_w * BBOX_B and bbox_h[
162
+ i] > max_h * BBOX_B:
163
+ new_frame_list.append(f)
164
+ else:
165
+ l_i, r_i = 0, len(frame_list) - 1
166
+ if len(b0) == 0:
167
+ continue
168
+ if b0[0] == 0:
169
+ for i in range(len(b0) - 1):
170
+ if b0[i] + 1 == b0[i + 1]:
171
+ l_i = b0[i + 1]
172
+ else:
173
+ break
174
+ if b0[-1] == len(frame_list) - 1:
175
+ for i in range(len(b0) - 1):
176
+ i = len(b0) - 1 - i
177
+ if b0[i] - 1 == b0[i - 1]:
178
+ r_i = b0[i - 1]
179
+ else:
180
+ break
181
+
182
+ max_lw, max_lh = bbox_w[l_i], bbox_h[l_i]
183
+ max_rw, max_rh = bbox_w[r_i], bbox_h[r_i]
184
+ for i, f in enumerate(frame_list):
185
+ if i < l_i:
186
+ if bbox_w[i] > max_lw * BBOX_B and bbox_h[
187
+ i] > max_lh * BBOX_B:
188
+ new_frame_list.append(f)
189
+ elif i > r_i:
190
+ if bbox_w[i] > max_rw * BBOX_B and bbox_h[
191
+ i] > max_rh * BBOX_B:
192
+ new_frame_list.append(f)
193
+ else:
194
+ new_frame_list.append(f)
195
+ new_tracklet_dict = dict()
196
+ for f in new_frame_list:
197
+ new_tracklet_dict[f] = tracklet_dict[f]
198
+ new_mot_list[tracklet] = new_tracklet_dict
199
+ else:
200
+ new_mot_list[tracklet] = tracklet_dict
201
+ return new_mot_list
202
+
203
+ def break_mot(self, mot_list, cid):
204
+ new_mot_list = dict()
205
+ new_num_tracklets = max(mot_list) + 1
206
+ for tracklet in mot_list:
207
+ tracklet_dict = mot_list[tracklet]
208
+ frame_list = list(tracklet_dict.keys())
209
+ frame_list.sort()
210
+ zone_list = []
211
+ back_tracklet = False
212
+ new_zone_f = 0
213
+ pre_frame = frame_list[0]
214
+ time_break = False
215
+ for f in frame_list:
216
+ if f - pre_frame > 100:
217
+ if cid in [44, 45]:
218
+ time_break = True
219
+ break
220
+ if not cid in [41, 44, 45, 46]:
221
+ break
222
+ pre_frame = f
223
+ new_zone = tracklet_dict[f]['zone']
224
+ if len(zone_list) > 0 and zone_list[-1] == new_zone:
225
+ continue
226
+ if new_zone_f > 1:
227
+ if len(zone_list) > 1 and new_zone in zone_list:
228
+ back_tracklet = True
229
+ zone_list.append(new_zone)
230
+ new_zone_f = 0
231
+ else:
232
+ new_zone_f += 1
233
+ if back_tracklet:
234
+ new_tracklet_dict = dict()
235
+ pre_bbox = -1
236
+ pre_arrow = 0
237
+ have_break = False
238
+ for f in frame_list:
239
+ now_bbox = tracklet_dict[f]['bbox']
240
+ if type(pre_bbox) == int:
241
+ if pre_bbox == -1:
242
+ pre_bbox = now_bbox
243
+ now_arrow = now_bbox[0] - pre_bbox[0]
244
+ if pre_arrow * now_arrow < 0 and len(
245
+ new_tracklet_dict) > 15 and not have_break:
246
+ new_mot_list[tracklet] = new_tracklet_dict
247
+ new_tracklet_dict = dict()
248
+ have_break = True
249
+ if have_break:
250
+ tracklet_dict[f]['id'] = new_num_tracklets
251
+ new_tracklet_dict[f] = tracklet_dict[f]
252
+ pre_bbox, pre_arrow = now_bbox, now_arrow
253
+
254
+ if have_break:
255
+ new_mot_list[new_num_tracklets] = new_tracklet_dict
256
+ new_num_tracklets += 1
257
+ else:
258
+ new_mot_list[tracklet] = new_tracklet_dict
259
+ elif time_break:
260
+ new_tracklet_dict = dict()
261
+ have_break = False
262
+ pre_frame = frame_list[0]
263
+ for f in frame_list:
264
+ if f - pre_frame > 100:
265
+ new_mot_list[tracklet] = new_tracklet_dict
266
+ new_tracklet_dict = dict()
267
+ have_break = True
268
+ new_tracklet_dict[f] = tracklet_dict[f]
269
+ pre_frame = f
270
+ if have_break:
271
+ new_mot_list[new_num_tracklets] = new_tracklet_dict
272
+ new_num_tracklets += 1
273
+ else:
274
+ new_mot_list[tracklet] = new_tracklet_dict
275
+ else:
276
+ new_mot_list[tracklet] = tracklet_dict
277
+ return new_mot_list
278
+
279
+ def intra_matching(self, mot_list, sub_mot_list):
280
+ sub_zone_dict = dict()
281
+ new_mot_list = dict()
282
+ new_mot_list, new_sub_mot_list = self.do_intra_matching2(mot_list,
283
+ sub_mot_list)
284
+ return new_mot_list
285
+
286
+ def do_intra_matching2(self, mot_list, sub_list):
287
+ new_zone_dict = dict()
288
+
289
+ def get_trac_info(tracklet1):
290
+ t1_f = list(tracklet1)
291
+ t1_f.sort()
292
+ t1_fs = t1_f[0]
293
+ t1_fe = t1_f[-1]
294
+ t1_zs = tracklet1[t1_fs]['zone']
295
+ t1_ze = tracklet1[t1_fe]['zone']
296
+ t1_boxs = tracklet1[t1_fs]['bbox']
297
+ t1_boxe = tracklet1[t1_fe]['bbox']
298
+ t1_boxs = [(t1_boxs[2] + t1_boxs[0]) / 2,
299
+ (t1_boxs[3] + t1_boxs[1]) / 2]
300
+ t1_boxe = [(t1_boxe[2] + t1_boxe[0]) / 2,
301
+ (t1_boxe[3] + t1_boxe[1]) / 2]
302
+ return t1_fs, t1_fe, t1_zs, t1_ze, t1_boxs, t1_boxe
303
+
304
+ for t1id in sub_list:
305
+ tracklet1 = sub_list[t1id]
306
+ if tracklet1 == -1:
307
+ continue
308
+ t1_fs, t1_fe, t1_zs, t1_ze, t1_boxs, t1_boxe = get_trac_info(
309
+ tracklet1)
310
+ sim_dict = dict()
311
+ for t2id in mot_list:
312
+ tracklet2 = mot_list[t2id]
313
+ t2_fs, t2_fe, t2_zs, t2_ze, t2_boxs, t2_boxe = get_trac_info(
314
+ tracklet2)
315
+ if t1_ze == t2_zs:
316
+ if abs(t2_fs - t1_fe) < 5 and abs(t2_boxe[0] - t1_boxs[
317
+ 0]) < 50 and abs(t2_boxe[1] - t1_boxs[1]) < 50:
318
+ t1_feat = tracklet1[t1_fe]['feat']
319
+ t2_feat = tracklet2[t2_fs]['feat']
320
+ sim_dict[t2id] = np.matmul(t1_feat, t2_feat)
321
+ if t1_zs == t2_ze:
322
+ if abs(t2_fe - t1_fs) < 5 and abs(t2_boxs[0] - t1_boxe[
323
+ 0]) < 50 and abs(t2_boxs[1] - t1_boxe[1]) < 50:
324
+ t1_feat = tracklet1[t1_fs]['feat']
325
+ t2_feat = tracklet2[t2_fe]['feat']
326
+ sim_dict[t2id] = np.matmul(t1_feat, t2_feat)
327
+ if len(sim_dict) > 0:
328
+ max_sim = 0
329
+ max_id = 0
330
+ for t2id in sim_dict:
331
+ if sim_dict[t2id] > max_sim:
332
+ sim_dict[t2id] = max_sim
333
+ max_id = t2id
334
+ if max_sim > 0.5:
335
+ t2 = mot_list[max_id]
336
+ for t1f in tracklet1:
337
+ if t1f not in t2:
338
+ tracklet1[t1f]['id'] = max_id
339
+ t2[t1f] = tracklet1[t1f]
340
+ mot_list[max_id] = t2
341
+ sub_list[t1id] = -1
342
+ return mot_list, sub_list
343
+
344
+ def do_intra_matching(self, sub_zone_dict, sub_zone):
345
+ new_zone_dict = dict()
346
+ id_list = list(sub_zone_dict)
347
+ id2index = dict()
348
+ for index, id in enumerate(id_list):
349
+ id2index[id] = index
350
+
351
+ def get_trac_info(tracklet1):
352
+ t1_f = list(tracklet1)
353
+ t1_f.sort()
354
+ t1_fs = t1_f[0]
355
+ t1_fe = t1_f[-1]
356
+ t1_zs = tracklet1[t1_fs]['zone']
357
+ t1_ze = tracklet1[t1_fe]['zone']
358
+ t1_boxs = tracklet1[t1_fs]['bbox']
359
+ t1_boxe = tracklet1[t1_fe]['bbox']
360
+ t1_boxs = [(t1_boxs[2] + t1_boxs[0]) / 2,
361
+ (t1_boxs[3] + t1_boxs[1]) / 2]
362
+ t1_boxe = [(t1_boxe[2] + t1_boxe[0]) / 2,
363
+ (t1_boxe[3] + t1_boxe[1]) / 2]
364
+ return t1_fs, t1_fe, t1_zs, t1_ze, t1_boxs, t1_boxe
365
+
366
+ sim_matrix = np.zeros([len(id_list), len(id_list)])
367
+
368
+ for t1id in sub_zone_dict:
369
+ tracklet1 = sub_zone_dict[t1id]
370
+ t1_fs, t1_fe, t1_zs, t1_ze, t1_boxs, t1_boxe = get_trac_info(
371
+ tracklet1)
372
+ t1_feat = tracklet1[t1_fe]['feat']
373
+ for t2id in sub_zone_dict:
374
+ if t1id == t2id:
375
+ continue
376
+ tracklet2 = sub_zone_dict[t2id]
377
+ t2_fs, t2_fe, t2_zs, t2_ze, t2_boxs, t2_boxe = get_trac_info(
378
+ tracklet2)
379
+ if t1_zs != t1_ze and t2_ze != t2_zs or t1_fe > t2_fs:
380
+ continue
381
+ if abs(t1_boxe[0] - t2_boxs[0]) > 50 or abs(t1_boxe[1] -
382
+ t2_boxs[1]) > 50:
383
+ continue
384
+ if t2_fs - t1_fe > 5:
385
+ continue
386
+ t2_feat = tracklet2[t2_fs]['feat']
387
+ sim_matrix[id2index[t1id], id2index[t2id]] = np.matmul(t1_feat,
388
+ t2_feat)
389
+ sim_matrix[id2index[t2id], id2index[t1id]] = np.matmul(t1_feat,
390
+ t2_feat)
391
+ sim_matrix = 1 - sim_matrix
392
+ cluster_labels = AgglomerativeClustering(
393
+ n_clusters=None,
394
+ distance_threshold=0.7,
395
+ affinity='precomputed',
396
+ linkage='complete').fit_predict(sim_matrix)
397
+ new_zone_dict = dict()
398
+ label2id = dict()
399
+ for index, label in enumerate(cluster_labels):
400
+ tracklet = sub_zone_dict[id_list[index]]
401
+ if label not in label2id:
402
+ new_id = tracklet[list(tracklet)[0]]
403
+ new_tracklet = dict()
404
+ else:
405
+ new_id = label2id[label]
406
+ new_tracklet = new_zone_dict[label2id[label]]
407
+ for tf in tracklet:
408
+ tracklet[tf]['id'] = new_id
409
+ new_tracklet[tf] = tracklet[tf]
410
+ new_zone_dict[label] = new_tracklet
411
+
412
+ return new_zone_dict
pptracking/python/mot/tracker/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import base_jde_tracker
16
+ from . import base_sde_tracker
17
+ from . import jde_tracker
18
+ from . import deepsort_tracker
19
+ from . import ocsort_tracker
20
+
21
+ from .base_jde_tracker import *
22
+ from .base_sde_tracker import *
23
+ from .jde_tracker import *
24
+ from .deepsort_tracker import *
25
+ from .ocsort_tracker import *
pptracking/python/mot/tracker/base_jde_tracker.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/Zhongdao/Towards-Realtime-MOT/blob/master/tracker/multitracker.py
16
+ """
17
+
18
+ import numpy as np
19
+ from collections import defaultdict
20
+ from collections import deque, OrderedDict
21
+ from ..matching import jde_matching as matching
22
+
23
+ __all__ = [
24
+ 'TrackState',
25
+ 'BaseTrack',
26
+ 'STrack',
27
+ 'joint_stracks',
28
+ 'sub_stracks',
29
+ 'remove_duplicate_stracks',
30
+ ]
31
+
32
+
33
+ class TrackState(object):
34
+ New = 0
35
+ Tracked = 1
36
+ Lost = 2
37
+ Removed = 3
38
+
39
+
40
+ class BaseTrack(object):
41
+ _count_dict = defaultdict(int) # support single class and multi classes
42
+
43
+ track_id = 0
44
+ is_activated = False
45
+ state = TrackState.New
46
+
47
+ history = OrderedDict()
48
+ features = []
49
+ curr_feat = None
50
+ score = 0
51
+ start_frame = 0
52
+ frame_id = 0
53
+ time_since_update = 0
54
+
55
+ # multi-camera
56
+ location = (np.inf, np.inf)
57
+
58
+ @property
59
+ def end_frame(self):
60
+ return self.frame_id
61
+
62
+ @staticmethod
63
+ def next_id(cls_id):
64
+ BaseTrack._count_dict[cls_id] += 1
65
+ return BaseTrack._count_dict[cls_id]
66
+
67
+ # @even: reset track id
68
+ @staticmethod
69
+ def init_count(num_classes):
70
+ """
71
+ Initiate _count for all object classes
72
+ :param num_classes:
73
+ """
74
+ for cls_id in range(num_classes):
75
+ BaseTrack._count_dict[cls_id] = 0
76
+
77
+ @staticmethod
78
+ def reset_track_count(cls_id):
79
+ BaseTrack._count_dict[cls_id] = 0
80
+
81
+ def activate(self, *args):
82
+ raise NotImplementedError
83
+
84
+ def predict(self):
85
+ raise NotImplementedError
86
+
87
+ def update(self, *args, **kwargs):
88
+ raise NotImplementedError
89
+
90
+ def mark_lost(self):
91
+ self.state = TrackState.Lost
92
+
93
+ def mark_removed(self):
94
+ self.state = TrackState.Removed
95
+
96
+
97
+ class STrack(BaseTrack):
98
+ def __init__(self, tlwh, score, cls_id, buff_size=30, temp_feat=None):
99
+ # wait activate
100
+ self._tlwh = np.asarray(tlwh, dtype=np.float)
101
+ self.score = score
102
+ self.cls_id = cls_id
103
+ self.track_len = 0
104
+
105
+ self.kalman_filter = None
106
+ self.mean, self.covariance = None, None
107
+ self.is_activated = False
108
+
109
+ self.use_reid = True if temp_feat is not None else False
110
+ if self.use_reid:
111
+ self.smooth_feat = None
112
+ self.update_features(temp_feat)
113
+ self.features = deque([], maxlen=buff_size)
114
+ self.alpha = 0.9
115
+
116
+ def update_features(self, feat):
117
+ # L2 normalizing, this function has no use for BYTETracker
118
+ feat /= np.linalg.norm(feat)
119
+ self.curr_feat = feat
120
+ if self.smooth_feat is None:
121
+ self.smooth_feat = feat
122
+ else:
123
+ self.smooth_feat = self.alpha * self.smooth_feat + (
124
+ 1.0 - self.alpha) * feat
125
+ self.features.append(feat)
126
+ self.smooth_feat /= np.linalg.norm(self.smooth_feat)
127
+
128
+ def predict(self):
129
+ mean_state = self.mean.copy()
130
+ if self.state != TrackState.Tracked:
131
+ mean_state[7] = 0
132
+ self.mean, self.covariance = self.kalman_filter.predict(
133
+ mean_state, self.covariance)
134
+
135
+ @staticmethod
136
+ def multi_predict(tracks, kalman_filter):
137
+ if len(tracks) > 0:
138
+ multi_mean = np.asarray([track.mean.copy() for track in tracks])
139
+ multi_covariance = np.asarray(
140
+ [track.covariance for track in tracks])
141
+ for i, st in enumerate(tracks):
142
+ if st.state != TrackState.Tracked:
143
+ multi_mean[i][7] = 0
144
+ multi_mean, multi_covariance = kalman_filter.multi_predict(
145
+ multi_mean, multi_covariance)
146
+ for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
147
+ tracks[i].mean = mean
148
+ tracks[i].covariance = cov
149
+
150
+ def reset_track_id(self):
151
+ self.reset_track_count(self.cls_id)
152
+
153
+ def activate(self, kalman_filter, frame_id):
154
+ """Start a new track"""
155
+ self.kalman_filter = kalman_filter
156
+ # update track id for the object class
157
+ self.track_id = self.next_id(self.cls_id)
158
+ self.mean, self.covariance = self.kalman_filter.initiate(
159
+ self.tlwh_to_xyah(self._tlwh))
160
+
161
+ self.track_len = 0
162
+ self.state = TrackState.Tracked # set flag 'tracked'
163
+
164
+ if frame_id == 1: # to record the first frame's detection result
165
+ self.is_activated = True
166
+
167
+ self.frame_id = frame_id
168
+ self.start_frame = frame_id
169
+
170
+ def re_activate(self, new_track, frame_id, new_id=False):
171
+ self.mean, self.covariance = self.kalman_filter.update(
172
+ self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh))
173
+ if self.use_reid:
174
+ self.update_features(new_track.curr_feat)
175
+ self.track_len = 0
176
+ self.state = TrackState.Tracked
177
+ self.is_activated = True
178
+ self.frame_id = frame_id
179
+ if new_id: # update track id for the object class
180
+ self.track_id = self.next_id(self.cls_id)
181
+
182
+ def update(self, new_track, frame_id, update_feature=True):
183
+ self.frame_id = frame_id
184
+ self.track_len += 1
185
+
186
+ new_tlwh = new_track.tlwh
187
+ self.mean, self.covariance = self.kalman_filter.update(
188
+ self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh))
189
+ self.state = TrackState.Tracked # set flag 'tracked'
190
+ self.is_activated = True # set flag 'activated'
191
+
192
+ self.score = new_track.score
193
+ if update_feature and self.use_reid:
194
+ self.update_features(new_track.curr_feat)
195
+
196
+ @property
197
+ def tlwh(self):
198
+ """Get current position in bounding box format `(top left x, top left y,
199
+ width, height)`.
200
+ """
201
+ if self.mean is None:
202
+ return self._tlwh.copy()
203
+
204
+ ret = self.mean[:4].copy()
205
+ ret[2] *= ret[3]
206
+ ret[:2] -= ret[2:] / 2
207
+ return ret
208
+
209
+ @property
210
+ def tlbr(self):
211
+ """Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
212
+ `(top left, bottom right)`.
213
+ """
214
+ ret = self.tlwh.copy()
215
+ ret[2:] += ret[:2]
216
+ return ret
217
+
218
+ @staticmethod
219
+ def tlwh_to_xyah(tlwh):
220
+ """Convert bounding box to format `(center x, center y, aspect ratio,
221
+ height)`, where the aspect ratio is `width / height`.
222
+ """
223
+ ret = np.asarray(tlwh).copy()
224
+ ret[:2] += ret[2:] / 2
225
+ ret[2] /= ret[3]
226
+ return ret
227
+
228
+ def to_xyah(self):
229
+ return self.tlwh_to_xyah(self.tlwh)
230
+
231
+ @staticmethod
232
+ def tlbr_to_tlwh(tlbr):
233
+ ret = np.asarray(tlbr).copy()
234
+ ret[2:] -= ret[:2]
235
+ return ret
236
+
237
+ @staticmethod
238
+ def tlwh_to_tlbr(tlwh):
239
+ ret = np.asarray(tlwh).copy()
240
+ ret[2:] += ret[:2]
241
+ return ret
242
+
243
+ def __repr__(self):
244
+ return 'OT_({}-{})_({}-{})'.format(self.cls_id, self.track_id,
245
+ self.start_frame, self.end_frame)
246
+
247
+
248
+ def joint_stracks(tlista, tlistb):
249
+ exists = {}
250
+ res = []
251
+ for t in tlista:
252
+ exists[t.track_id] = 1
253
+ res.append(t)
254
+ for t in tlistb:
255
+ tid = t.track_id
256
+ if not exists.get(tid, 0):
257
+ exists[tid] = 1
258
+ res.append(t)
259
+ return res
260
+
261
+
262
+ def sub_stracks(tlista, tlistb):
263
+ stracks = {}
264
+ for t in tlista:
265
+ stracks[t.track_id] = t
266
+ for t in tlistb:
267
+ tid = t.track_id
268
+ if stracks.get(tid, 0):
269
+ del stracks[tid]
270
+ return list(stracks.values())
271
+
272
+
273
+ def remove_duplicate_stracks(stracksa, stracksb):
274
+ pdist = matching.iou_distance(stracksa, stracksb)
275
+ pairs = np.where(pdist < 0.15)
276
+ dupa, dupb = list(), list()
277
+ for p, q in zip(*pairs):
278
+ timep = stracksa[p].frame_id - stracksa[p].start_frame
279
+ timeq = stracksb[q].frame_id - stracksb[q].start_frame
280
+ if timep > timeq:
281
+ dupb.append(q)
282
+ else:
283
+ dupa.append(p)
284
+ resa = [t for i, t in enumerate(stracksa) if not i in dupa]
285
+ resb = [t for i, t in enumerate(stracksb) if not i in dupb]
286
+ return resa, resb
pptracking/python/mot/tracker/base_sde_tracker.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/nwojke/deep_sort/blob/master/deep_sort/track.py
16
+ """
17
+
18
+ import datetime
19
+
20
+ __all__ = ['TrackState', 'Track']
21
+
22
+
23
+ class TrackState(object):
24
+ """
25
+ Enumeration type for the single target track state. Newly created tracks are
26
+ classified as `tentative` until enough evidence has been collected. Then,
27
+ the track state is changed to `confirmed`. Tracks that are no longer alive
28
+ are classified as `deleted` to mark them for removal from the set of active
29
+ tracks.
30
+ """
31
+ Tentative = 1
32
+ Confirmed = 2
33
+ Deleted = 3
34
+
35
+
36
+ class Track(object):
37
+ """
38
+ A single target track with state space `(x, y, a, h)` and associated
39
+ velocities, where `(x, y)` is the center of the bounding box, `a` is the
40
+ aspect ratio and `h` is the height.
41
+
42
+ Args:
43
+ mean (ndarray): Mean vector of the initial state distribution.
44
+ covariance (ndarray): Covariance matrix of the initial state distribution.
45
+ track_id (int): A unique track identifier.
46
+ n_init (int): Number of consecutive detections before the track is confirmed.
47
+ The track state is set to `Deleted` if a miss occurs within the first
48
+ `n_init` frames.
49
+ max_age (int): The maximum number of consecutive misses before the track
50
+ state is set to `Deleted`.
51
+ cls_id (int): The category id of the tracked box.
52
+ score (float): The confidence score of the tracked box.
53
+ feature (Optional[ndarray]): Feature vector of the detection this track
54
+ originates from. If not None, this feature is added to the `features` cache.
55
+
56
+ Attributes:
57
+ hits (int): Total number of measurement updates.
58
+ age (int): Total number of frames since first occurance.
59
+ time_since_update (int): Total number of frames since last measurement
60
+ update.
61
+ state (TrackState): The current track state.
62
+ features (List[ndarray]): A cache of features. On each measurement update,
63
+ the associated feature vector is added to this list.
64
+ """
65
+
66
+ def __init__(self,
67
+ mean,
68
+ covariance,
69
+ track_id,
70
+ n_init,
71
+ max_age,
72
+ cls_id,
73
+ score,
74
+ feature=None):
75
+ self.mean = mean
76
+ self.covariance = covariance
77
+ self.track_id = track_id
78
+ self.hits = 1
79
+ self.age = 1
80
+ self.time_since_update = 0
81
+ self.cls_id = cls_id
82
+ self.score = score
83
+ self.start_time = datetime.datetime.now()
84
+
85
+ self.state = TrackState.Tentative
86
+ self.features = []
87
+ self.feat = feature
88
+ if feature is not None:
89
+ self.features.append(feature)
90
+
91
+ self._n_init = n_init
92
+ self._max_age = max_age
93
+
94
+ def to_tlwh(self):
95
+ """Get position in format `(top left x, top left y, width, height)`."""
96
+ ret = self.mean[:4].copy()
97
+ ret[2] *= ret[3]
98
+ ret[:2] -= ret[2:] / 2
99
+ return ret
100
+
101
+ def to_tlbr(self):
102
+ """Get position in bounding box format `(min x, miny, max x, max y)`."""
103
+ ret = self.to_tlwh()
104
+ ret[2:] = ret[:2] + ret[2:]
105
+ return ret
106
+
107
+ def predict(self, kalman_filter):
108
+ """
109
+ Propagate the state distribution to the current time step using a Kalman
110
+ filter prediction step.
111
+ """
112
+ self.mean, self.covariance = kalman_filter.predict(self.mean,
113
+ self.covariance)
114
+ self.age += 1
115
+ self.time_since_update += 1
116
+
117
+ def update(self, kalman_filter, detection):
118
+ """
119
+ Perform Kalman filter measurement update step and update the associated
120
+ detection feature cache.
121
+ """
122
+ self.mean, self.covariance = kalman_filter.update(self.mean,
123
+ self.covariance,
124
+ detection.to_xyah())
125
+ self.features.append(detection.feature)
126
+ self.feat = detection.feature
127
+ self.cls_id = detection.cls_id
128
+ self.score = detection.score
129
+
130
+ self.hits += 1
131
+ self.time_since_update = 0
132
+ if self.state == TrackState.Tentative and self.hits >= self._n_init:
133
+ self.state = TrackState.Confirmed
134
+
135
+ def mark_missed(self):
136
+ """Mark this track as missed (no association at the current time step).
137
+ """
138
+ if self.state == TrackState.Tentative:
139
+ self.state = TrackState.Deleted
140
+ elif self.time_since_update > self._max_age:
141
+ self.state = TrackState.Deleted
142
+
143
+ def is_tentative(self):
144
+ """Returns True if this track is tentative (unconfirmed)."""
145
+ return self.state == TrackState.Tentative
146
+
147
+ def is_confirmed(self):
148
+ """Returns True if this track is confirmed."""
149
+ return self.state == TrackState.Confirmed
150
+
151
+ def is_deleted(self):
152
+ """Returns True if this track is dead and should be deleted."""
153
+ return self.state == TrackState.Deleted
pptracking/python/mot/tracker/deepsort_tracker.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/nwojke/deep_sort/blob/master/deep_sort/tracker.py
16
+ """
17
+
18
+ import numpy as np
19
+
20
+ from ..motion import KalmanFilter
21
+ from ..matching.deepsort_matching import NearestNeighborDistanceMetric
22
+ from ..matching.deepsort_matching import iou_cost, min_cost_matching, matching_cascade, gate_cost_matrix
23
+ from .base_sde_tracker import Track
24
+ from ..utils import Detection
25
+
26
+ __all__ = ['DeepSORTTracker']
27
+
28
+
29
+ class DeepSORTTracker(object):
30
+ """
31
+ DeepSORT tracker
32
+
33
+ Args:
34
+ input_size (list): input feature map size to reid model, [h, w] format,
35
+ [64, 192] as default.
36
+ min_box_area (int): min box area to filter out low quality boxes
37
+ vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
38
+ bad results, set 1.6 default for pedestrian tracking. If set <=0
39
+ means no need to filter bboxes.
40
+ budget (int): If not None, fix samples per class to at most this number.
41
+ Removes the oldest samples when the budget is reached.
42
+ max_age (int): maximum number of missed misses before a track is deleted
43
+ n_init (float): Number of frames that a track remains in initialization
44
+ phase. Number of consecutive detections before the track is confirmed.
45
+ The track state is set to `Deleted` if a miss occurs within the first
46
+ `n_init` frames.
47
+ metric_type (str): either "euclidean" or "cosine", the distance metric
48
+ used for measurement to track association.
49
+ matching_threshold (float): samples with larger distance are
50
+ considered an invalid match.
51
+ max_iou_distance (float): max iou distance threshold
52
+ motion (object): KalmanFilter instance
53
+ """
54
+
55
+ def __init__(self,
56
+ input_size=[64, 192],
57
+ min_box_area=0,
58
+ vertical_ratio=-1,
59
+ budget=100,
60
+ max_age=70,
61
+ n_init=3,
62
+ metric_type='cosine',
63
+ matching_threshold=0.2,
64
+ max_iou_distance=0.9,
65
+ motion='KalmanFilter'):
66
+ self.input_size = input_size
67
+ self.min_box_area = min_box_area
68
+ self.vertical_ratio = vertical_ratio
69
+ self.max_age = max_age
70
+ self.n_init = n_init
71
+ self.metric = NearestNeighborDistanceMetric(metric_type,
72
+ matching_threshold, budget)
73
+ self.max_iou_distance = max_iou_distance
74
+ if motion == 'KalmanFilter':
75
+ self.motion = KalmanFilter()
76
+
77
+ self.tracks = []
78
+ self._next_id = 1
79
+
80
+ def predict(self):
81
+ """
82
+ Propagate track state distributions one time step forward.
83
+ This function should be called once every time step, before `update`.
84
+ """
85
+ for track in self.tracks:
86
+ track.predict(self.motion)
87
+
88
+ def update(self, pred_dets, pred_embs):
89
+ """
90
+ Perform measurement update and track management.
91
+ Args:
92
+ pred_dets (np.array): Detection results of the image, the shape is
93
+ [N, 6], means 'cls_id, score, x0, y0, x1, y1'.
94
+ pred_embs (np.array): Embedding results of the image, the shape is
95
+ [N, 128], usually pred_embs.shape[1] is a multiple of 128.
96
+ """
97
+ pred_cls_ids = pred_dets[:, 0:1]
98
+ pred_scores = pred_dets[:, 1:2]
99
+ pred_xyxys = pred_dets[:, 2:6]
100
+ pred_tlwhs = np.concatenate(
101
+ (pred_xyxys[:, 0:2], pred_xyxys[:, 2:4] - pred_xyxys[:, 0:2] + 1),
102
+ axis=1)
103
+
104
+ detections = [
105
+ Detection(tlwh, score, feat, cls_id)
106
+ for tlwh, score, feat, cls_id in zip(pred_tlwhs, pred_scores,
107
+ pred_embs, pred_cls_ids)
108
+ ]
109
+
110
+ # Run matching cascade.
111
+ matches, unmatched_tracks, unmatched_detections = \
112
+ self._match(detections)
113
+
114
+ # Update track set.
115
+ for track_idx, detection_idx in matches:
116
+ self.tracks[track_idx].update(self.motion,
117
+ detections[detection_idx])
118
+ for track_idx in unmatched_tracks:
119
+ self.tracks[track_idx].mark_missed()
120
+ for detection_idx in unmatched_detections:
121
+ self._initiate_track(detections[detection_idx])
122
+ self.tracks = [t for t in self.tracks if not t.is_deleted()]
123
+
124
+ # Update distance metric.
125
+ active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
126
+ features, targets = [], []
127
+ for track in self.tracks:
128
+ if not track.is_confirmed():
129
+ continue
130
+ features += track.features
131
+ targets += [track.track_id for _ in track.features]
132
+ track.features = []
133
+ self.metric.partial_fit(
134
+ np.asarray(features), np.asarray(targets), active_targets)
135
+ output_stracks = self.tracks
136
+ return output_stracks
137
+
138
+ def _match(self, detections):
139
+ def gated_metric(tracks, dets, track_indices, detection_indices):
140
+ features = np.array([dets[i].feature for i in detection_indices])
141
+ targets = np.array([tracks[i].track_id for i in track_indices])
142
+ cost_matrix = self.metric.distance(features, targets)
143
+ cost_matrix = gate_cost_matrix(self.motion, cost_matrix, tracks,
144
+ dets, track_indices,
145
+ detection_indices)
146
+ return cost_matrix
147
+
148
+ # Split track set into confirmed and unconfirmed tracks.
149
+ confirmed_tracks = [
150
+ i for i, t in enumerate(self.tracks) if t.is_confirmed()
151
+ ]
152
+ unconfirmed_tracks = [
153
+ i for i, t in enumerate(self.tracks) if not t.is_confirmed()
154
+ ]
155
+
156
+ # Associate confirmed tracks using appearance features.
157
+ matches_a, unmatched_tracks_a, unmatched_detections = \
158
+ matching_cascade(
159
+ gated_metric, self.metric.matching_threshold, self.max_age,
160
+ self.tracks, detections, confirmed_tracks)
161
+
162
+ # Associate remaining tracks together with unconfirmed tracks using IOU.
163
+ iou_track_candidates = unconfirmed_tracks + [
164
+ k for k in unmatched_tracks_a
165
+ if self.tracks[k].time_since_update == 1
166
+ ]
167
+ unmatched_tracks_a = [
168
+ k for k in unmatched_tracks_a
169
+ if self.tracks[k].time_since_update != 1
170
+ ]
171
+ matches_b, unmatched_tracks_b, unmatched_detections = \
172
+ min_cost_matching(
173
+ iou_cost, self.max_iou_distance, self.tracks,
174
+ detections, iou_track_candidates, unmatched_detections)
175
+
176
+ matches = matches_a + matches_b
177
+ unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
178
+ return matches, unmatched_tracks, unmatched_detections
179
+
180
+ def _initiate_track(self, detection):
181
+ mean, covariance = self.motion.initiate(detection.to_xyah())
182
+ self.tracks.append(
183
+ Track(mean, covariance, self._next_id, self.n_init, self.max_age,
184
+ detection.cls_id, detection.score, detection.feature))
185
+ self._next_id += 1
pptracking/python/mot/tracker/jde_tracker.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/Zhongdao/Towards-Realtime-MOT/blob/master/tracker/multitracker.py
16
+ """
17
+
18
+ import numpy as np
19
+ from collections import defaultdict
20
+
21
+ from ..matching import jde_matching as matching
22
+ from ..motion import KalmanFilter
23
+ from .base_jde_tracker import TrackState, STrack
24
+ from .base_jde_tracker import joint_stracks, sub_stracks, remove_duplicate_stracks
25
+
26
+ __all__ = ['JDETracker']
27
+
28
+
29
+ class JDETracker(object):
30
+ __shared__ = ['num_classes']
31
+ """
32
+ JDE tracker, support single class and multi classes
33
+
34
+ Args:
35
+ use_byte (bool): Whether use ByteTracker, default False
36
+ num_classes (int): the number of classes
37
+ det_thresh (float): threshold of detection score
38
+ track_buffer (int): buffer for tracker
39
+ min_box_area (int): min box area to filter out low quality boxes
40
+ vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
41
+ bad results. If set <= 0 means no need to filter bboxes,usually set
42
+ 1.6 for pedestrian tracking.
43
+ tracked_thresh (float): linear assignment threshold of tracked
44
+ stracks and detections
45
+ r_tracked_thresh (float): linear assignment threshold of
46
+ tracked stracks and unmatched detections
47
+ unconfirmed_thresh (float): linear assignment threshold of
48
+ unconfirmed stracks and unmatched detections
49
+ conf_thres (float): confidence threshold for tracking, also used in
50
+ ByteTracker as higher confidence threshold
51
+ match_thres (float): linear assignment threshold of tracked
52
+ stracks and detections in ByteTracker
53
+ low_conf_thres (float): lower confidence threshold for tracking in
54
+ ByteTracker
55
+ input_size (list): input feature map size to reid model, [h, w] format,
56
+ [64, 192] as default.
57
+ motion (str): motion model, KalmanFilter as default
58
+ metric_type (str): either "euclidean" or "cosine", the distance metric
59
+ used for measurement to track association.
60
+ """
61
+
62
+ def __init__(self,
63
+ use_byte=False,
64
+ num_classes=1,
65
+ det_thresh=0.3,
66
+ track_buffer=30,
67
+ min_box_area=0,
68
+ vertical_ratio=0,
69
+ tracked_thresh=0.7,
70
+ r_tracked_thresh=0.5,
71
+ unconfirmed_thresh=0.7,
72
+ conf_thres=0,
73
+ match_thres=0.8,
74
+ low_conf_thres=0.2,
75
+ input_size=[64, 192],
76
+ motion='KalmanFilter',
77
+ metric_type='euclidean'):
78
+ self.use_byte = use_byte
79
+ self.num_classes = num_classes
80
+ self.det_thresh = det_thresh if not use_byte else conf_thres + 0.1
81
+ self.track_buffer = track_buffer
82
+ self.min_box_area = min_box_area
83
+ self.vertical_ratio = vertical_ratio
84
+
85
+ self.tracked_thresh = tracked_thresh
86
+ self.r_tracked_thresh = r_tracked_thresh
87
+ self.unconfirmed_thresh = unconfirmed_thresh
88
+ self.conf_thres = conf_thres
89
+ self.match_thres = match_thres
90
+ self.low_conf_thres = low_conf_thres
91
+
92
+ self.input_size = input_size
93
+ if motion == 'KalmanFilter':
94
+ self.motion = KalmanFilter()
95
+ self.metric_type = metric_type
96
+
97
+ self.frame_id = 0
98
+ self.tracked_tracks_dict = defaultdict(list) # dict(list[STrack])
99
+ self.lost_tracks_dict = defaultdict(list) # dict(list[STrack])
100
+ self.removed_tracks_dict = defaultdict(list) # dict(list[STrack])
101
+
102
+ self.max_time_lost = 0
103
+ # max_time_lost will be calculated: int(frame_rate / 30.0 * track_buffer)
104
+
105
+ def update(self, pred_dets, pred_embs=None):
106
+ """
107
+ Processes the image frame and finds bounding box(detections).
108
+ Associates the detection with corresponding tracklets and also handles
109
+ lost, removed, refound and active tracklets.
110
+
111
+ Args:
112
+ pred_dets (np.array): Detection results of the image, the shape is
113
+ [N, 6], means 'cls_id, score, x0, y0, x1, y1'.
114
+ pred_embs (np.array): Embedding results of the image, the shape is
115
+ [N, 128] or [N, 512].
116
+
117
+ Return:
118
+ output_stracks_dict (dict(list)): The list contains information
119
+ regarding the online_tracklets for the received image tensor.
120
+ """
121
+ self.frame_id += 1
122
+ if self.frame_id == 1:
123
+ STrack.init_count(self.num_classes)
124
+ activated_tracks_dict = defaultdict(list)
125
+ refined_tracks_dict = defaultdict(list)
126
+ lost_tracks_dict = defaultdict(list)
127
+ removed_tracks_dict = defaultdict(list)
128
+ output_tracks_dict = defaultdict(list)
129
+
130
+ pred_dets_dict = defaultdict(list)
131
+ pred_embs_dict = defaultdict(list)
132
+
133
+ # unify single and multi classes detection and embedding results
134
+ for cls_id in range(self.num_classes):
135
+ cls_idx = (pred_dets[:, 0:1] == cls_id).squeeze(-1)
136
+ pred_dets_dict[cls_id] = pred_dets[cls_idx]
137
+ if pred_embs is not None:
138
+ pred_embs_dict[cls_id] = pred_embs[cls_idx]
139
+ else:
140
+ pred_embs_dict[cls_id] = None
141
+
142
+ for cls_id in range(self.num_classes):
143
+ """ Step 1: Get detections by class"""
144
+ pred_dets_cls = pred_dets_dict[cls_id]
145
+ pred_embs_cls = pred_embs_dict[cls_id]
146
+ remain_inds = (pred_dets_cls[:, 1:2] > self.conf_thres).squeeze(-1)
147
+ if remain_inds.sum() > 0:
148
+ pred_dets_cls = pred_dets_cls[remain_inds]
149
+ if pred_embs_cls is None:
150
+ # in original ByteTrack
151
+ detections = [
152
+ STrack(
153
+ STrack.tlbr_to_tlwh(tlbrs[2:6]),
154
+ tlbrs[1],
155
+ cls_id,
156
+ 30,
157
+ temp_feat=None) for tlbrs in pred_dets_cls
158
+ ]
159
+ else:
160
+ pred_embs_cls = pred_embs_cls[remain_inds]
161
+ detections = [
162
+ STrack(
163
+ STrack.tlbr_to_tlwh(tlbrs[2:6]), tlbrs[1], cls_id,
164
+ 30, temp_feat)
165
+ for (tlbrs, temp_feat
166
+ ) in zip(pred_dets_cls, pred_embs_cls)
167
+ ]
168
+ else:
169
+ detections = []
170
+ ''' Add newly detected tracklets to tracked_stracks'''
171
+ unconfirmed_dict = defaultdict(list)
172
+ tracked_tracks_dict = defaultdict(list)
173
+ for track in self.tracked_tracks_dict[cls_id]:
174
+ if not track.is_activated:
175
+ # previous tracks which are not active in the current frame are added in unconfirmed list
176
+ unconfirmed_dict[cls_id].append(track)
177
+ else:
178
+ # Active tracks are added to the local list 'tracked_stracks'
179
+ tracked_tracks_dict[cls_id].append(track)
180
+ """ Step 2: First association, with embedding"""
181
+ # building tracking pool for the current frame
182
+ track_pool_dict = defaultdict(list)
183
+ track_pool_dict[cls_id] = joint_stracks(
184
+ tracked_tracks_dict[cls_id], self.lost_tracks_dict[cls_id])
185
+
186
+ # Predict the current location with KalmanFilter
187
+ STrack.multi_predict(track_pool_dict[cls_id], self.motion)
188
+
189
+ if pred_embs_cls is None:
190
+ # in original ByteTrack
191
+ dists = matching.iou_distance(track_pool_dict[cls_id],
192
+ detections)
193
+ matches, u_track, u_detection = matching.linear_assignment(
194
+ dists, thresh=self.match_thres) # not self.tracked_thresh
195
+ else:
196
+ dists = matching.embedding_distance(
197
+ track_pool_dict[cls_id],
198
+ detections,
199
+ metric=self.metric_type)
200
+ dists = matching.fuse_motion(
201
+ self.motion, dists, track_pool_dict[cls_id], detections)
202
+ matches, u_track, u_detection = matching.linear_assignment(
203
+ dists, thresh=self.tracked_thresh)
204
+
205
+ for i_tracked, idet in matches:
206
+ # i_tracked is the id of the track and idet is the detection
207
+ track = track_pool_dict[cls_id][i_tracked]
208
+ det = detections[idet]
209
+ if track.state == TrackState.Tracked:
210
+ # If the track is active, add the detection to the track
211
+ track.update(detections[idet], self.frame_id)
212
+ activated_tracks_dict[cls_id].append(track)
213
+ else:
214
+ # We have obtained a detection from a track which is not active,
215
+ # hence put the track in refind_stracks list
216
+ track.re_activate(det, self.frame_id, new_id=False)
217
+ refined_tracks_dict[cls_id].append(track)
218
+
219
+ # None of the steps below happen if there are no undetected tracks.
220
+ """ Step 3: Second association, with IOU"""
221
+ if self.use_byte:
222
+ inds_low = pred_dets_dict[cls_id][:, 1:2] > self.low_conf_thres
223
+ inds_high = pred_dets_dict[cls_id][:, 1:2] < self.conf_thres
224
+ inds_second = np.logical_and(inds_low, inds_high).squeeze(-1)
225
+ pred_dets_cls_second = pred_dets_dict[cls_id][inds_second]
226
+
227
+ # association the untrack to the low score detections
228
+ if len(pred_dets_cls_second) > 0:
229
+ if pred_embs_dict[cls_id] is None:
230
+ # in original ByteTrack
231
+ detections_second = [
232
+ STrack(
233
+ STrack.tlbr_to_tlwh(tlbrs[2:6]),
234
+ tlbrs[1],
235
+ cls_id,
236
+ 30,
237
+ temp_feat=None)
238
+ for tlbrs in pred_dets_cls_second
239
+ ]
240
+ else:
241
+ pred_embs_cls_second = pred_embs_dict[cls_id][
242
+ inds_second]
243
+ detections_second = [
244
+ STrack(
245
+ STrack.tlbr_to_tlwh(tlbrs[2:6]), tlbrs[1],
246
+ cls_id, 30, temp_feat)
247
+ for (tlbrs, temp_feat) in zip(pred_dets_cls_second,
248
+ pred_embs_cls_second)
249
+ ]
250
+ else:
251
+ detections_second = []
252
+ r_tracked_stracks = [
253
+ track_pool_dict[cls_id][i] for i in u_track
254
+ if track_pool_dict[cls_id][i].state == TrackState.Tracked
255
+ ]
256
+ dists = matching.iou_distance(r_tracked_stracks,
257
+ detections_second)
258
+ matches, u_track, u_detection_second = matching.linear_assignment(
259
+ dists, thresh=0.4) # not r_tracked_thresh
260
+ else:
261
+ detections = [detections[i] for i in u_detection]
262
+ r_tracked_stracks = []
263
+ for i in u_track:
264
+ if track_pool_dict[cls_id][i].state == TrackState.Tracked:
265
+ r_tracked_stracks.append(track_pool_dict[cls_id][i])
266
+ dists = matching.iou_distance(r_tracked_stracks, detections)
267
+
268
+ matches, u_track, u_detection = matching.linear_assignment(
269
+ dists, thresh=self.r_tracked_thresh)
270
+
271
+ for i_tracked, idet in matches:
272
+ track = r_tracked_stracks[i_tracked]
273
+ det = detections[
274
+ idet] if not self.use_byte else detections_second[idet]
275
+ if track.state == TrackState.Tracked:
276
+ track.update(det, self.frame_id)
277
+ activated_tracks_dict[cls_id].append(track)
278
+ else:
279
+ track.re_activate(det, self.frame_id, new_id=False)
280
+ refined_tracks_dict[cls_id].append(track)
281
+
282
+ for it in u_track:
283
+ track = r_tracked_stracks[it]
284
+ if not track.state == TrackState.Lost:
285
+ track.mark_lost()
286
+ lost_tracks_dict[cls_id].append(track)
287
+ '''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
288
+ detections = [detections[i] for i in u_detection]
289
+ dists = matching.iou_distance(unconfirmed_dict[cls_id], detections)
290
+ matches, u_unconfirmed, u_detection = matching.linear_assignment(
291
+ dists, thresh=self.unconfirmed_thresh)
292
+ for i_tracked, idet in matches:
293
+ unconfirmed_dict[cls_id][i_tracked].update(detections[idet],
294
+ self.frame_id)
295
+ activated_tracks_dict[cls_id].append(unconfirmed_dict[cls_id][
296
+ i_tracked])
297
+ for it in u_unconfirmed:
298
+ track = unconfirmed_dict[cls_id][it]
299
+ track.mark_removed()
300
+ removed_tracks_dict[cls_id].append(track)
301
+ """ Step 4: Init new stracks"""
302
+ for inew in u_detection:
303
+ track = detections[inew]
304
+ if track.score < self.det_thresh:
305
+ continue
306
+ track.activate(self.motion, self.frame_id)
307
+ activated_tracks_dict[cls_id].append(track)
308
+ """ Step 5: Update state"""
309
+ for track in self.lost_tracks_dict[cls_id]:
310
+ if self.frame_id - track.end_frame > self.max_time_lost:
311
+ track.mark_removed()
312
+ removed_tracks_dict[cls_id].append(track)
313
+
314
+ self.tracked_tracks_dict[cls_id] = [
315
+ t for t in self.tracked_tracks_dict[cls_id]
316
+ if t.state == TrackState.Tracked
317
+ ]
318
+ self.tracked_tracks_dict[cls_id] = joint_stracks(
319
+ self.tracked_tracks_dict[cls_id],
320
+ activated_tracks_dict[cls_id])
321
+ self.tracked_tracks_dict[cls_id] = joint_stracks(
322
+ self.tracked_tracks_dict[cls_id], refined_tracks_dict[cls_id])
323
+ self.lost_tracks_dict[cls_id] = sub_stracks(
324
+ self.lost_tracks_dict[cls_id],
325
+ self.tracked_tracks_dict[cls_id])
326
+ self.lost_tracks_dict[cls_id].extend(lost_tracks_dict[cls_id])
327
+ self.lost_tracks_dict[cls_id] = sub_stracks(
328
+ self.lost_tracks_dict[cls_id],
329
+ self.removed_tracks_dict[cls_id])
330
+ self.removed_tracks_dict[cls_id].extend(removed_tracks_dict[
331
+ cls_id])
332
+ self.tracked_tracks_dict[cls_id], self.lost_tracks_dict[
333
+ cls_id] = remove_duplicate_stracks(
334
+ self.tracked_tracks_dict[cls_id],
335
+ self.lost_tracks_dict[cls_id])
336
+
337
+ # get scores of lost tracks
338
+ output_tracks_dict[cls_id] = [
339
+ track for track in self.tracked_tracks_dict[cls_id]
340
+ if track.is_activated
341
+ ]
342
+
343
+ return output_tracks_dict
pptracking/python/mot/tracker/ocsort_tracker.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This code is based on https://github.com/noahcao/OC_SORT/blob/master/trackers/ocsort_tracker/ocsort.py
16
+ """
17
+
18
+ import numpy as np
19
+ try:
20
+ from filterpy.kalman import KalmanFilter
21
+ except:
22
+ print(
23
+ 'Warning: Unable to use OC-SORT, please install filterpy, for example: `pip install filterpy`, see https://github.com/rlabbe/filterpy'
24
+ )
25
+ pass
26
+
27
+ from ..matching.ocsort_matching import associate, linear_assignment, iou_batch
28
+
29
+
30
+ def k_previous_obs(observations, cur_age, k):
31
+ if len(observations) == 0:
32
+ return [-1, -1, -1, -1, -1]
33
+ for i in range(k):
34
+ dt = k - i
35
+ if cur_age - dt in observations:
36
+ return observations[cur_age - dt]
37
+ max_age = max(observations.keys())
38
+ return observations[max_age]
39
+
40
+
41
+ def convert_bbox_to_z(bbox):
42
+ """
43
+ Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form
44
+ [x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is
45
+ the aspect ratio
46
+ """
47
+ w = bbox[2] - bbox[0]
48
+ h = bbox[3] - bbox[1]
49
+ x = bbox[0] + w / 2.
50
+ y = bbox[1] + h / 2.
51
+ s = w * h # scale is just area
52
+ r = w / float(h + 1e-6)
53
+ return np.array([x, y, s, r]).reshape((4, 1))
54
+
55
+
56
+ def convert_x_to_bbox(x, score=None):
57
+ """
58
+ Takes a bounding box in the centre form [x,y,s,r] and returns it in the form
59
+ [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right
60
+ """
61
+ w = np.sqrt(x[2] * x[3])
62
+ h = x[2] / w
63
+ if (score == None):
64
+ return np.array(
65
+ [x[0] - w / 2., x[1] - h / 2., x[0] + w / 2.,
66
+ x[1] + h / 2.]).reshape((1, 4))
67
+ else:
68
+ score = np.array([score])
69
+ return np.array([
70
+ x[0] - w / 2., x[1] - h / 2., x[0] + w / 2., x[1] + h / 2., score
71
+ ]).reshape((1, 5))
72
+
73
+
74
+ def speed_direction(bbox1, bbox2):
75
+ cx1, cy1 = (bbox1[0] + bbox1[2]) / 2.0, (bbox1[1] + bbox1[3]) / 2.0
76
+ cx2, cy2 = (bbox2[0] + bbox2[2]) / 2.0, (bbox2[1] + bbox2[3]) / 2.0
77
+ speed = np.array([cy2 - cy1, cx2 - cx1])
78
+ norm = np.sqrt((cy2 - cy1)**2 + (cx2 - cx1)**2) + 1e-6
79
+ return speed / norm
80
+
81
+
82
+ class KalmanBoxTracker(object):
83
+ """
84
+ This class represents the internal state of individual tracked objects observed as bbox.
85
+
86
+ Args:
87
+ bbox (np.array): bbox in [x1,y1,x2,y2,score] format.
88
+ delta_t (int): delta_t of previous observation
89
+ """
90
+ count = 0
91
+
92
+ def __init__(self, bbox, delta_t=3):
93
+ try:
94
+ from filterpy.kalman import KalmanFilter
95
+ except Exception as e:
96
+ raise RuntimeError(
97
+ 'Unable to use OC-SORT, please install filterpy, for example: `pip install filterpy`, see https://github.com/rlabbe/filterpy'
98
+ )
99
+ self.kf = KalmanFilter(dim_x=7, dim_z=4)
100
+ self.kf.F = np.array([[1, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1, 0],
101
+ [0, 0, 1, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0],
102
+ [0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1, 0],
103
+ [0, 0, 0, 0, 0, 0, 1]])
104
+ self.kf.H = np.array([[1, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0],
105
+ [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0]])
106
+ self.kf.R[2:, 2:] *= 10.
107
+ self.kf.P[4:, 4:] *= 1000.
108
+ # give high uncertainty to the unobservable initial velocities
109
+ self.kf.P *= 10.
110
+ self.kf.Q[-1, -1] *= 0.01
111
+ self.kf.Q[4:, 4:] *= 0.01
112
+
113
+ self.score = bbox[4]
114
+ self.kf.x[:4] = convert_bbox_to_z(bbox)
115
+ self.time_since_update = 0
116
+ self.id = KalmanBoxTracker.count
117
+ KalmanBoxTracker.count += 1
118
+ self.history = []
119
+ self.hits = 0
120
+ self.hit_streak = 0
121
+ self.age = 0
122
+ """
123
+ NOTE: [-1,-1,-1,-1,-1] is a compromising placeholder for non-observation status, the same for the return of
124
+ function k_previous_obs. It is ugly and I do not like it. But to support generate observation array in a
125
+ fast and unified way, which you would see below k_observations = np.array([k_previous_obs(...]]), let's bear it for now.
126
+ """
127
+ self.last_observation = np.array([-1, -1, -1, -1, -1]) # placeholder
128
+ self.observations = dict()
129
+ self.history_observations = []
130
+ self.velocity = None
131
+ self.delta_t = delta_t
132
+
133
+ def update(self, bbox):
134
+ """
135
+ Updates the state vector with observed bbox.
136
+ """
137
+ if bbox is not None:
138
+ if self.last_observation.sum() >= 0: # no previous observation
139
+ previous_box = None
140
+ for i in range(self.delta_t):
141
+ dt = self.delta_t - i
142
+ if self.age - dt in self.observations:
143
+ previous_box = self.observations[self.age - dt]
144
+ break
145
+ if previous_box is None:
146
+ previous_box = self.last_observation
147
+ """
148
+ Estimate the track speed direction with observations \Delta t steps away
149
+ """
150
+ self.velocity = speed_direction(previous_box, bbox)
151
+ """
152
+ Insert new observations. This is a ugly way to maintain both self.observations
153
+ and self.history_observations. Bear it for the moment.
154
+ """
155
+ self.last_observation = bbox
156
+ self.observations[self.age] = bbox
157
+ self.history_observations.append(bbox)
158
+
159
+ self.time_since_update = 0
160
+ self.history = []
161
+ self.hits += 1
162
+ self.hit_streak += 1
163
+ self.kf.update(convert_bbox_to_z(bbox))
164
+ else:
165
+ self.kf.update(bbox)
166
+
167
+ def predict(self):
168
+ """
169
+ Advances the state vector and returns the predicted bounding box estimate.
170
+ """
171
+ if ((self.kf.x[6] + self.kf.x[2]) <= 0):
172
+ self.kf.x[6] *= 0.0
173
+
174
+ self.kf.predict()
175
+ self.age += 1
176
+ if (self.time_since_update > 0):
177
+ self.hit_streak = 0
178
+ self.time_since_update += 1
179
+ self.history.append(convert_x_to_bbox(self.kf.x, score=self.score))
180
+ return self.history[-1]
181
+
182
+ def get_state(self):
183
+ return convert_x_to_bbox(self.kf.x, score=self.score)
184
+
185
+
186
+ class OCSORTTracker(object):
187
+ """
188
+ OCSORT tracker, support single class
189
+
190
+ Args:
191
+ det_thresh (float): threshold of detection score
192
+ max_age (int): maximum number of missed misses before a track is deleted
193
+ min_hits (int): minimum hits for associate
194
+ iou_threshold (float): iou threshold for associate
195
+ delta_t (int): delta_t of previous observation
196
+ inertia (float): vdc_weight of angle_diff_cost for associate
197
+ vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
198
+ bad results. If set <= 0 means no need to filter bboxes,usually set
199
+ 1.6 for pedestrian tracking.
200
+ min_box_area (int): min box area to filter out low quality boxes
201
+ use_byte (bool): Whether use ByteTracker, default False
202
+ """
203
+
204
+ def __init__(self,
205
+ det_thresh=0.6,
206
+ max_age=30,
207
+ min_hits=3,
208
+ iou_threshold=0.3,
209
+ delta_t=3,
210
+ inertia=0.2,
211
+ vertical_ratio=-1,
212
+ min_box_area=0,
213
+ use_byte=False):
214
+ self.det_thresh = det_thresh
215
+ self.max_age = max_age
216
+ self.min_hits = min_hits
217
+ self.iou_threshold = iou_threshold
218
+ self.delta_t = delta_t
219
+ self.inertia = inertia
220
+ self.vertical_ratio = vertical_ratio
221
+ self.min_box_area = min_box_area
222
+ self.use_byte = use_byte
223
+
224
+ self.trackers = []
225
+ self.frame_count = 0
226
+ KalmanBoxTracker.count = 0
227
+
228
+ def update(self, pred_dets, pred_embs=None):
229
+ """
230
+ Args:
231
+ pred_dets (np.array): Detection results of the image, the shape is
232
+ [N, 6], means 'cls_id, score, x0, y0, x1, y1'.
233
+ pred_embs (np.array): Embedding results of the image, the shape is
234
+ [N, 128] or [N, 512], default as None.
235
+
236
+ Return:
237
+ tracking boxes (np.array): [M, 6], means 'x0, y0, x1, y1, score, id'.
238
+ """
239
+ if pred_dets is None:
240
+ return np.empty((0, 6))
241
+
242
+ self.frame_count += 1
243
+
244
+ bboxes = pred_dets[:, 2:]
245
+ scores = pred_dets[:, 1:2]
246
+ dets = np.concatenate((bboxes, scores), axis=1)
247
+ scores = scores.squeeze(-1)
248
+
249
+ inds_low = scores > 0.1
250
+ inds_high = scores < self.det_thresh
251
+ inds_second = np.logical_and(inds_low, inds_high)
252
+ # self.det_thresh > score > 0.1, for second matching
253
+ dets_second = dets[inds_second] # detections for second matching
254
+ remain_inds = scores > self.det_thresh
255
+ dets = dets[remain_inds]
256
+
257
+ # get predicted locations from existing trackers.
258
+ trks = np.zeros((len(self.trackers), 5))
259
+ to_del = []
260
+ ret = []
261
+ for t, trk in enumerate(trks):
262
+ pos = self.trackers[t].predict()[0]
263
+ trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
264
+ if np.any(np.isnan(pos)):
265
+ to_del.append(t)
266
+ trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
267
+ for t in reversed(to_del):
268
+ self.trackers.pop(t)
269
+
270
+ velocities = np.array([
271
+ trk.velocity if trk.velocity is not None else np.array((0, 0))
272
+ for trk in self.trackers
273
+ ])
274
+ last_boxes = np.array([trk.last_observation for trk in self.trackers])
275
+ k_observations = np.array([
276
+ k_previous_obs(trk.observations, trk.age, self.delta_t)
277
+ for trk in self.trackers
278
+ ])
279
+ """
280
+ First round of association
281
+ """
282
+ matched, unmatched_dets, unmatched_trks = associate(
283
+ dets, trks, self.iou_threshold, velocities, k_observations,
284
+ self.inertia)
285
+ for m in matched:
286
+ self.trackers[m[1]].update(dets[m[0], :])
287
+ """
288
+ Second round of associaton by OCR
289
+ """
290
+ # BYTE association
291
+ if self.use_byte and len(dets_second) > 0 and unmatched_trks.shape[
292
+ 0] > 0:
293
+ u_trks = trks[unmatched_trks]
294
+ iou_left = iou_batch(
295
+ dets_second,
296
+ u_trks) # iou between low score detections and unmatched tracks
297
+ iou_left = np.array(iou_left)
298
+ if iou_left.max() > self.iou_threshold:
299
+ """
300
+ NOTE: by using a lower threshold, e.g., self.iou_threshold - 0.1, you may
301
+ get a higher performance especially on MOT17/MOT20 datasets. But we keep it
302
+ uniform here for simplicity
303
+ """
304
+ matched_indices = linear_assignment(-iou_left)
305
+ to_remove_trk_indices = []
306
+ for m in matched_indices:
307
+ det_ind, trk_ind = m[0], unmatched_trks[m[1]]
308
+ if iou_left[m[0], m[1]] < self.iou_threshold:
309
+ continue
310
+ self.trackers[trk_ind].update(dets_second[det_ind, :])
311
+ to_remove_trk_indices.append(trk_ind)
312
+ unmatched_trks = np.setdiff1d(unmatched_trks,
313
+ np.array(to_remove_trk_indices))
314
+
315
+ if unmatched_dets.shape[0] > 0 and unmatched_trks.shape[0] > 0:
316
+ left_dets = dets[unmatched_dets]
317
+ left_trks = last_boxes[unmatched_trks]
318
+ iou_left = iou_batch(left_dets, left_trks)
319
+ iou_left = np.array(iou_left)
320
+ if iou_left.max() > self.iou_threshold:
321
+ """
322
+ NOTE: by using a lower threshold, e.g., self.iou_threshold - 0.1, you may
323
+ get a higher performance especially on MOT17/MOT20 datasets. But we keep it
324
+ uniform here for simplicity
325
+ """
326
+ rematched_indices = linear_assignment(-iou_left)
327
+ to_remove_det_indices = []
328
+ to_remove_trk_indices = []
329
+ for m in rematched_indices:
330
+ det_ind, trk_ind = unmatched_dets[m[0]], unmatched_trks[m[
331
+ 1]]
332
+ if iou_left[m[0], m[1]] < self.iou_threshold:
333
+ continue
334
+ self.trackers[trk_ind].update(dets[det_ind, :])
335
+ to_remove_det_indices.append(det_ind)
336
+ to_remove_trk_indices.append(trk_ind)
337
+ unmatched_dets = np.setdiff1d(unmatched_dets,
338
+ np.array(to_remove_det_indices))
339
+ unmatched_trks = np.setdiff1d(unmatched_trks,
340
+ np.array(to_remove_trk_indices))
341
+
342
+ for m in unmatched_trks:
343
+ self.trackers[m].update(None)
344
+
345
+ # create and initialise new trackers for unmatched detections
346
+ for i in unmatched_dets:
347
+ trk = KalmanBoxTracker(dets[i, :], delta_t=self.delta_t)
348
+ self.trackers.append(trk)
349
+ i = len(self.trackers)
350
+ for trk in reversed(self.trackers):
351
+ if trk.last_observation.sum() < 0:
352
+ d = trk.get_state()[0]
353
+ else:
354
+ d = trk.last_observation # tlbr + score
355
+ if (trk.time_since_update < 1) and (
356
+ trk.hit_streak >= self.min_hits or
357
+ self.frame_count <= self.min_hits):
358
+ # +1 as MOT benchmark requires positive
359
+ ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1))
360
+ i -= 1
361
+ # remove dead tracklet
362
+ if (trk.time_since_update > self.max_age):
363
+ self.trackers.pop(i)
364
+ if (len(ret) > 0):
365
+ return np.concatenate(ret)
366
+ return np.empty((0, 6))
pptracking/python/mot/utils.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import cv2
17
+ import time
18
+ import numpy as np
19
+ import collections
20
+ import math
21
+
22
+ __all__ = [
23
+ 'MOTTimer', 'Detection', 'write_mot_results', 'load_det_results',
24
+ 'preprocess_reid', 'get_crops', 'clip_box', 'scale_coords',
25
+ 'flow_statistic', 'update_object_info'
26
+ ]
27
+
28
+
29
+ class MOTTimer(object):
30
+ """
31
+ This class used to compute and print the current FPS while evaling.
32
+ """
33
+
34
+ def __init__(self, window_size=20):
35
+ self.start_time = 0.
36
+ self.diff = 0.
37
+ self.duration = 0.
38
+ self.deque = collections.deque(maxlen=window_size)
39
+
40
+ def tic(self):
41
+ # using time.time instead of time.clock because time time.clock
42
+ # does not normalize for multithreading
43
+ self.start_time = time.time()
44
+
45
+ def toc(self, average=True):
46
+ self.diff = time.time() - self.start_time
47
+ self.deque.append(self.diff)
48
+ if average:
49
+ self.duration = np.mean(self.deque)
50
+ else:
51
+ self.duration = np.sum(self.deque)
52
+ return self.duration
53
+
54
+ def clear(self):
55
+ self.start_time = 0.
56
+ self.diff = 0.
57
+ self.duration = 0.
58
+
59
+
60
+ class Detection(object):
61
+ """
62
+ This class represents a bounding box detection in a single image.
63
+
64
+ Args:
65
+ tlwh (Tensor): Bounding box in format `(top left x, top left y,
66
+ width, height)`.
67
+ score (Tensor): Bounding box confidence score.
68
+ feature (Tensor): A feature vector that describes the object
69
+ contained in this image.
70
+ cls_id (Tensor): Bounding box category id.
71
+ """
72
+
73
+ def __init__(self, tlwh, score, feature, cls_id):
74
+ self.tlwh = np.asarray(tlwh, dtype=np.float32)
75
+ self.score = float(score)
76
+ self.feature = np.asarray(feature, dtype=np.float32)
77
+ self.cls_id = int(cls_id)
78
+
79
+ def to_tlbr(self):
80
+ """
81
+ Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
82
+ `(top left, bottom right)`.
83
+ """
84
+ ret = self.tlwh.copy()
85
+ ret[2:] += ret[:2]
86
+ return ret
87
+
88
+ def to_xyah(self):
89
+ """
90
+ Convert bounding box to format `(center x, center y, aspect ratio,
91
+ height)`, where the aspect ratio is `width / height`.
92
+ """
93
+ ret = self.tlwh.copy()
94
+ ret[:2] += ret[2:] / 2
95
+ ret[2] /= ret[3]
96
+ return ret
97
+
98
+
99
+ def write_mot_results(filename, results, data_type='mot', num_classes=1):
100
+ # support single and multi classes
101
+ if data_type in ['mot', 'mcmot']:
102
+ save_format = '{frame},{id},{x1},{y1},{w},{h},{score},{cls_id},-1,-1\n'
103
+ elif data_type == 'kitti':
104
+ save_format = '{frame} {id} car 0 0 -10 {x1} {y1} {x2} {y2} -10 -10 -10 -1000 -1000 -1000 -10\n'
105
+ else:
106
+ raise ValueError(data_type)
107
+
108
+ f = open(filename, 'w')
109
+ for cls_id in range(num_classes):
110
+ for frame_id, tlwhs, tscores, track_ids in results[cls_id]:
111
+ if data_type == 'kitti':
112
+ frame_id -= 1
113
+ for tlwh, score, track_id in zip(tlwhs, tscores, track_ids):
114
+ if track_id < 0: continue
115
+ if data_type == 'mot':
116
+ cls_id = -1
117
+
118
+ x1, y1, w, h = tlwh
119
+ x2, y2 = x1 + w, y1 + h
120
+ line = save_format.format(
121
+ frame=frame_id,
122
+ id=track_id,
123
+ x1=x1,
124
+ y1=y1,
125
+ x2=x2,
126
+ y2=y2,
127
+ w=w,
128
+ h=h,
129
+ score=score,
130
+ cls_id=cls_id)
131
+ f.write(line)
132
+ print('MOT results save in {}'.format(filename))
133
+
134
+
135
+ def load_det_results(det_file, num_frames):
136
+ assert os.path.exists(det_file) and os.path.isfile(det_file), \
137
+ '{} is not exist or not a file.'.format(det_file)
138
+ labels = np.loadtxt(det_file, dtype='float32', delimiter=',')
139
+ assert labels.shape[1] == 7, \
140
+ "Each line of {} should have 7 items: '[frame_id],[x0],[y0],[w],[h],[score],[class_id]'.".format(det_file)
141
+ results_list = []
142
+ for frame_i in range(num_frames):
143
+ results = {'bbox': [], 'score': [], 'cls_id': []}
144
+ lables_with_frame = labels[labels[:, 0] == frame_i + 1]
145
+ # each line of lables_with_frame:
146
+ # [frame_id],[x0],[y0],[w],[h],[score],[class_id]
147
+ for l in lables_with_frame:
148
+ results['bbox'].append(l[1:5])
149
+ results['score'].append(l[5:6])
150
+ results['cls_id'].append(l[6:7])
151
+ results_list.append(results)
152
+ return results_list
153
+
154
+
155
+ def scale_coords(coords, input_shape, im_shape, scale_factor):
156
+ # Note: ratio has only one value, scale_factor[0] == scale_factor[1]
157
+ #
158
+ # This function only used for JDE YOLOv3 or other detectors with
159
+ # LetterBoxResize and JDEBBoxPostProcess, coords output from detector had
160
+ # not scaled back to the origin image.
161
+
162
+ ratio = scale_factor[0]
163
+ pad_w = (input_shape[1] - int(im_shape[1])) / 2
164
+ pad_h = (input_shape[0] - int(im_shape[0])) / 2
165
+ coords[:, 0::2] -= pad_w
166
+ coords[:, 1::2] -= pad_h
167
+ coords[:, 0:4] /= ratio
168
+ coords[:, :4] = np.clip(coords[:, :4], a_min=0, a_max=coords[:, :4].max())
169
+ return coords.round()
170
+
171
+
172
+ def clip_box(xyxy, ori_image_shape):
173
+ H, W = ori_image_shape
174
+ xyxy[:, 0::2] = np.clip(xyxy[:, 0::2], a_min=0, a_max=W)
175
+ xyxy[:, 1::2] = np.clip(xyxy[:, 1::2], a_min=0, a_max=H)
176
+ w = xyxy[:, 2:3] - xyxy[:, 0:1]
177
+ h = xyxy[:, 3:4] - xyxy[:, 1:2]
178
+ mask = np.logical_and(h > 0, w > 0)
179
+ keep_idx = np.nonzero(mask)
180
+ return xyxy[keep_idx[0]], keep_idx
181
+
182
+
183
+ def get_crops(xyxy, ori_img, w, h):
184
+ crops = []
185
+ xyxy = xyxy.astype(np.int64)
186
+ ori_img = ori_img.transpose(1, 0, 2) # [h,w,3]->[w,h,3]
187
+ for i, bbox in enumerate(xyxy):
188
+ crop = ori_img[bbox[0]:bbox[2], bbox[1]:bbox[3], :]
189
+ crops.append(crop)
190
+ crops = preprocess_reid(crops, w, h)
191
+ return crops
192
+
193
+
194
+ def preprocess_reid(imgs,
195
+ w=64,
196
+ h=192,
197
+ mean=[0.485, 0.456, 0.406],
198
+ std=[0.229, 0.224, 0.225]):
199
+ im_batch = []
200
+ for img in imgs:
201
+ img = cv2.resize(img, (w, h))
202
+ img = img[:, :, ::-1].astype('float32').transpose((2, 0, 1)) / 255
203
+ img_mean = np.array(mean).reshape((3, 1, 1))
204
+ img_std = np.array(std).reshape((3, 1, 1))
205
+ img -= img_mean
206
+ img /= img_std
207
+ img = np.expand_dims(img, axis=0)
208
+ im_batch.append(img)
209
+ im_batch = np.concatenate(im_batch, 0)
210
+ return im_batch
211
+
212
+
213
+ def flow_statistic(result,
214
+ secs_interval,
215
+ do_entrance_counting,
216
+ do_break_in_counting,
217
+ region_type,
218
+ video_fps,
219
+ entrance,
220
+ id_set,
221
+ interval_id_set,
222
+ in_id_list,
223
+ out_id_list,
224
+ prev_center,
225
+ records,
226
+ data_type='mot',
227
+ ids2names=['pedestrian']):
228
+ # Count in/out number:
229
+ # Note that 'region_type' should be one of ['horizontal', 'vertical', 'custom'],
230
+ # 'horizontal' and 'vertical' means entrance is the center line as the entrance when do_entrance_counting,
231
+ # 'custom' means entrance is a region defined by users when do_break_in_counting.
232
+
233
+ if do_entrance_counting:
234
+ assert region_type in [
235
+ 'horizontal', 'vertical'
236
+ ], "region_type should be 'horizontal' or 'vertical' when do entrance counting."
237
+ entrance_x, entrance_y = entrance[0], entrance[1]
238
+ frame_id, tlwhs, tscores, track_ids = result
239
+ for tlwh, score, track_id in zip(tlwhs, tscores, track_ids):
240
+ if track_id < 0: continue
241
+ if data_type == 'kitti':
242
+ frame_id -= 1
243
+ x1, y1, w, h = tlwh
244
+ center_x = x1 + w / 2.
245
+ center_y = y1 + h / 2.
246
+ if track_id in prev_center:
247
+ if region_type == 'horizontal':
248
+ # horizontal center line
249
+ if prev_center[track_id][1] <= entrance_y and \
250
+ center_y > entrance_y:
251
+ in_id_list.append(track_id)
252
+ if prev_center[track_id][1] >= entrance_y and \
253
+ center_y < entrance_y:
254
+ out_id_list.append(track_id)
255
+ else:
256
+ # vertical center line
257
+ if prev_center[track_id][0] <= entrance_x and \
258
+ center_x > entrance_x:
259
+ in_id_list.append(track_id)
260
+ if prev_center[track_id][0] >= entrance_x and \
261
+ center_x < entrance_x:
262
+ out_id_list.append(track_id)
263
+ prev_center[track_id][0] = center_x
264
+ prev_center[track_id][1] = center_y
265
+ else:
266
+ prev_center[track_id] = [center_x, center_y]
267
+
268
+ if do_break_in_counting:
269
+ assert region_type in [
270
+ 'custom'
271
+ ], "region_type should be 'custom' when do break_in counting."
272
+ assert len(
273
+ entrance
274
+ ) >= 4, "entrance should be at least 3 points and (w,h) of image when do break_in counting."
275
+ im_w, im_h = entrance[-1][:]
276
+ entrance = np.array(entrance[:-1])
277
+
278
+ frame_id, tlwhs, tscores, track_ids = result
279
+ for tlwh, score, track_id in zip(tlwhs, tscores, track_ids):
280
+ if track_id < 0: continue
281
+ if data_type == 'kitti':
282
+ frame_id -= 1
283
+ x1, y1, w, h = tlwh
284
+ center_x = min(x1 + w / 2., im_w - 1)
285
+ if ids2names[0] == 'pedestrian':
286
+ center_y = min(y1 + h, im_h - 1)
287
+ else:
288
+ center_y = min(y1 + h / 2, im_h - 1)
289
+
290
+ # counting objects in region of the first frame
291
+ if frame_id == 1:
292
+ if in_quadrangle([center_x, center_y], entrance, im_h, im_w):
293
+ in_id_list.append(-1)
294
+ else:
295
+ prev_center[track_id] = [center_x, center_y]
296
+ else:
297
+ if track_id in prev_center:
298
+ if not in_quadrangle(prev_center[track_id], entrance, im_h,
299
+ im_w) and in_quadrangle(
300
+ [center_x, center_y], entrance,
301
+ im_h, im_w):
302
+ in_id_list.append(track_id)
303
+ prev_center[track_id] = [center_x, center_y]
304
+ else:
305
+ prev_center[track_id] = [center_x, center_y]
306
+
307
+ # Count totol number, number at a manual-setting interval
308
+ frame_id, tlwhs, tscores, track_ids = result
309
+ for tlwh, score, track_id in zip(tlwhs, tscores, track_ids):
310
+ if track_id < 0: continue
311
+ id_set.add(track_id)
312
+ interval_id_set.add(track_id)
313
+
314
+ # Reset counting at the interval beginning
315
+ if frame_id % video_fps == 0 and frame_id / video_fps % secs_interval == 0:
316
+ curr_interval_count = len(interval_id_set)
317
+ interval_id_set.clear()
318
+ info = "Frame id: {}, Total count: {}".format(frame_id, len(id_set))
319
+ if do_entrance_counting:
320
+ info += ", In count: {}, Out count: {}".format(
321
+ len(in_id_list), len(out_id_list))
322
+ if do_break_in_counting:
323
+ info += ", Break_in count: {}".format(len(in_id_list))
324
+ if frame_id % video_fps == 0 and frame_id / video_fps % secs_interval == 0:
325
+ info += ", Count during {} secs: {}".format(secs_interval,
326
+ curr_interval_count)
327
+ interval_id_set.clear()
328
+ # print(info)
329
+ info += "\n"
330
+ records.append(info)
331
+
332
+ return {
333
+ "id_set": id_set,
334
+ "interval_id_set": interval_id_set,
335
+ "in_id_list": in_id_list,
336
+ "out_id_list": out_id_list,
337
+ "prev_center": prev_center,
338
+ "records": records,
339
+ }
340
+
341
+
342
+ def distance(center_1, center_2):
343
+ return math.sqrt(
344
+ math.pow(center_1[0] - center_2[0], 2) + math.pow(center_1[1] -
345
+ center_2[1], 2))
346
+
347
+
348
+ # update vehicle parking info
349
+ def update_object_info(object_in_region_info,
350
+ result,
351
+ region_type,
352
+ entrance,
353
+ fps,
354
+ illegal_parking_time,
355
+ distance_threshold_frame=3,
356
+ distance_threshold_interval=50):
357
+ '''
358
+ For consecutive frames, the distance between two frame is smaller than distance_threshold_frame, regard as parking
359
+ For parking in general, the move distance should smaller than distance_threshold_interval
360
+ The moving distance of the vehicle is scaled according to the y, which is inversely proportional to y.
361
+ '''
362
+
363
+ assert region_type in [
364
+ 'custom'
365
+ ], "region_type should be 'custom' when do break_in counting."
366
+ assert len(
367
+ entrance
368
+ ) >= 4, "entrance should be at least 3 points and (w,h) of image when do break_in counting."
369
+
370
+ frame_id, tlwhs, tscores, track_ids = result # result from mot
371
+
372
+ im_w, im_h = entrance[-1][:]
373
+ entrance = np.array(entrance[:-1])
374
+
375
+ illegal_parking_dict = {}
376
+ for tlwh, score, track_id in zip(tlwhs, tscores, track_ids):
377
+ if track_id < 0: continue
378
+
379
+ x1, y1, w, h = tlwh
380
+ center_x = min(x1 + w / 2., im_w - 1)
381
+ center_y = min(y1 + h / 2, im_h - 1)
382
+
383
+ if not in_quadrangle([center_x, center_y], entrance, im_h, im_w):
384
+ continue
385
+
386
+ current_center = (center_x, center_y)
387
+ if track_id not in object_in_region_info.keys(
388
+ ): # first time appear in region
389
+ object_in_region_info[track_id] = {}
390
+ object_in_region_info[track_id]["start_frame"] = frame_id
391
+ object_in_region_info[track_id]["end_frame"] = frame_id
392
+ object_in_region_info[track_id]["prev_center"] = current_center
393
+ object_in_region_info[track_id]["start_center"] = current_center
394
+ else:
395
+ prev_center = object_in_region_info[track_id]["prev_center"]
396
+
397
+ dis = distance(current_center, prev_center)
398
+ scaled_dis = 200 * dis / (
399
+ current_center[1] + 1) # scale distance according to y
400
+ dis = scaled_dis
401
+
402
+ if dis < distance_threshold_frame: # not move
403
+ object_in_region_info[track_id]["end_frame"] = frame_id
404
+ object_in_region_info[track_id]["prev_center"] = current_center
405
+ else: # move
406
+ object_in_region_info[track_id]["start_frame"] = frame_id
407
+ object_in_region_info[track_id]["end_frame"] = frame_id
408
+ object_in_region_info[track_id]["prev_center"] = current_center
409
+ object_in_region_info[track_id][
410
+ "start_center"] = current_center
411
+
412
+ # whether current object parking
413
+ distance_from_start = distance(
414
+ object_in_region_info[track_id]["start_center"], current_center)
415
+ if distance_from_start > distance_threshold_interval:
416
+ # moved
417
+ object_in_region_info[track_id]["start_frame"] = frame_id
418
+ object_in_region_info[track_id]["end_frame"] = frame_id
419
+ object_in_region_info[track_id]["prev_center"] = current_center
420
+ object_in_region_info[track_id]["start_center"] = current_center
421
+ continue
422
+
423
+ if (object_in_region_info[track_id]["end_frame"]-object_in_region_info[track_id]["start_frame"]) /fps >= illegal_parking_time \
424
+ and distance_from_start<distance_threshold_interval:
425
+ illegal_parking_dict[track_id] = {"bbox": [x1, y1, w, h]}
426
+
427
+ return object_in_region_info, illegal_parking_dict
428
+
429
+
430
+ def in_quadrangle(point, entrance, im_h, im_w):
431
+ mask = np.zeros((im_h, im_w, 1), np.uint8)
432
+ cv2.fillPoly(mask, [entrance], 255)
433
+ p = tuple(map(int, point))
434
+ if mask[p[1], p[0], :] > 0:
435
+ return True
436
+ else:
437
+ return False
pptracking/python/mot/visualize.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import division
16
+
17
+ import os
18
+ import cv2
19
+ import numpy as np
20
+ from PIL import Image, ImageDraw, ImageFile
21
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
22
+ from collections import deque
23
+
24
+
25
+ def visualize_box_mask(im, results, labels, threshold=0.5):
26
+ """
27
+ Args:
28
+ im (str/np.ndarray): path of image/np.ndarray read by cv2
29
+ results (dict): include 'boxes': np.ndarray: shape:[N,6], N: number of box,
30
+ matix element:[class, score, x_min, y_min, x_max, y_max]
31
+ labels (list): labels:['class1', ..., 'classn']
32
+ threshold (float): Threshold of score.
33
+ Returns:
34
+ im (PIL.Image.Image): visualized image
35
+ """
36
+ if isinstance(im, str):
37
+ im = Image.open(im).convert('RGB')
38
+ else:
39
+ im = Image.fromarray(im)
40
+ if 'boxes' in results and len(results['boxes']) > 0:
41
+ im = draw_box(im, results['boxes'], labels, threshold=threshold)
42
+ return im
43
+
44
+
45
+ def get_color_map_list(num_classes):
46
+ """
47
+ Args:
48
+ num_classes (int): number of class
49
+ Returns:
50
+ color_map (list): RGB color list
51
+ """
52
+ color_map = num_classes * [0, 0, 0]
53
+ for i in range(0, num_classes):
54
+ j = 0
55
+ lab = i
56
+ while lab:
57
+ color_map[i * 3] |= (((lab >> 0) & 1) << (7 - j))
58
+ color_map[i * 3 + 1] |= (((lab >> 1) & 1) << (7 - j))
59
+ color_map[i * 3 + 2] |= (((lab >> 2) & 1) << (7 - j))
60
+ j += 1
61
+ lab >>= 3
62
+ color_map = [color_map[i:i + 3] for i in range(0, len(color_map), 3)]
63
+ return color_map
64
+
65
+
66
+ def draw_box(im, np_boxes, labels, threshold=0.5):
67
+ """
68
+ Args:
69
+ im (PIL.Image.Image): PIL image
70
+ np_boxes (np.ndarray): shape:[N,6], N: number of box,
71
+ matix element:[class, score, x_min, y_min, x_max, y_max]
72
+ labels (list): labels:['class1', ..., 'classn']
73
+ threshold (float): threshold of box
74
+ Returns:
75
+ im (PIL.Image.Image): visualized image
76
+ """
77
+ draw_thickness = min(im.size) // 320
78
+ draw = ImageDraw.Draw(im)
79
+ clsid2color = {}
80
+ color_list = get_color_map_list(len(labels))
81
+ expect_boxes = (np_boxes[:, 1] > threshold) & (np_boxes[:, 0] > -1)
82
+ np_boxes = np_boxes[expect_boxes, :]
83
+
84
+ for dt in np_boxes:
85
+ clsid, bbox, score = int(dt[0]), dt[2:], dt[1]
86
+ if clsid not in clsid2color:
87
+ clsid2color[clsid] = color_list[clsid]
88
+ color = tuple(clsid2color[clsid])
89
+
90
+ if len(bbox) == 4:
91
+ xmin, ymin, xmax, ymax = bbox
92
+ print('class_id:{:d}, confidence:{:.4f}, left_top:[{:.2f},{:.2f}],'
93
+ 'right_bottom:[{:.2f},{:.2f}]'.format(
94
+ int(clsid), score, xmin, ymin, xmax, ymax))
95
+ # draw bbox
96
+ draw.line(
97
+ [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin),
98
+ (xmin, ymin)],
99
+ width=draw_thickness,
100
+ fill=color)
101
+ elif len(bbox) == 8:
102
+ x1, y1, x2, y2, x3, y3, x4, y4 = bbox
103
+ draw.line(
104
+ [(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)],
105
+ width=2,
106
+ fill=color)
107
+ xmin = min(x1, x2, x3, x4)
108
+ ymin = min(y1, y2, y3, y4)
109
+
110
+ # draw label
111
+ text = "{} {:.4f}".format(labels[clsid], score)
112
+ tw, th = draw.textsize(text)
113
+ draw.rectangle(
114
+ [(xmin + 1, ymin - th), (xmin + tw + 1, ymin)], fill=color)
115
+ draw.text((xmin + 1, ymin - th), text, fill=(255, 255, 255))
116
+ return im
117
+
118
+
119
+ def get_color(idx):
120
+ idx = idx * 3
121
+ color = ((37 * idx) % 255, (17 * idx) % 255, (29 * idx) % 255)
122
+ return color
123
+
124
+
125
+ def plot_tracking(image,
126
+ tlwhs,
127
+ obj_ids,
128
+ scores=None,
129
+ frame_id=0,
130
+ fps=0.,
131
+ ids2names=[],
132
+ do_entrance_counting=False,
133
+ entrance=None):
134
+ im = np.ascontiguousarray(np.copy(image))
135
+ im_h, im_w = im.shape[:2]
136
+
137
+ text_scale = max(0.5, image.shape[1] / 3000.)
138
+ text_thickness = 2
139
+ line_thickness = max(1, int(image.shape[1] / 500.))
140
+
141
+ cv2.putText(
142
+ im,
143
+ 'frame: %d fps: %.2f num: %d' % (frame_id, fps, len(tlwhs)),
144
+ (0, int(15 * text_scale) + 5),
145
+ cv2.FONT_ITALIC,
146
+ text_scale, (0, 0, 255),
147
+ thickness=text_thickness)
148
+ for i, tlwh in enumerate(tlwhs):
149
+ x1, y1, w, h = tlwh
150
+ intbox = tuple(map(int, (x1, y1, x1 + w, y1 + h)))
151
+ obj_id = int(obj_ids[i])
152
+ id_text = 'ID: {}'.format(int(obj_id))
153
+ if ids2names != []:
154
+ assert len(
155
+ ids2names) == 1, "plot_tracking only supports single classes."
156
+ id_text = 'ID: {}_'.format(ids2names[0]) + id_text
157
+ _line_thickness = 1 if obj_id <= 0 else line_thickness
158
+ color = get_color(abs(obj_id))
159
+ cv2.rectangle(
160
+ im,
161
+ intbox[0:2],
162
+ intbox[2:4],
163
+ color=color,
164
+ thickness=line_thickness)
165
+ cv2.putText(
166
+ im,
167
+ id_text, (intbox[0], intbox[1] - 25),
168
+ cv2.FONT_ITALIC,
169
+ text_scale, (0, 255, 255),
170
+ thickness=text_thickness)
171
+
172
+ if scores is not None:
173
+ text = 'score: {:.2f}'.format(float(scores[i]))
174
+ cv2.putText(
175
+ im,
176
+ text, (intbox[0], intbox[1] - 6),
177
+ cv2.FONT_ITALIC,
178
+ text_scale, (0, 255, 0),
179
+ thickness=text_thickness)
180
+ if do_entrance_counting:
181
+ entrance_line = tuple(map(int, entrance))
182
+ cv2.rectangle(
183
+ im,
184
+ entrance_line[0:2],
185
+ entrance_line[2:4],
186
+ color=(0, 255, 255),
187
+ thickness=line_thickness)
188
+ return im
189
+
190
+
191
+ def plot_tracking_dict(image,
192
+ num_classes,
193
+ tlwhs_dict,
194
+ obj_ids_dict,
195
+ scores_dict,
196
+ frame_id=0,
197
+ fps=0.,
198
+ ids2names=[],
199
+ do_entrance_counting=False,
200
+ do_break_in_counting=False,
201
+ do_illegal_parking_recognition=False,
202
+ illegal_parking_dict=None,
203
+ entrance=None,
204
+ records=None,
205
+ center_traj=None):
206
+ im = np.ascontiguousarray(np.copy(image))
207
+ im_h, im_w = im.shape[:2]
208
+ if do_break_in_counting or do_illegal_parking_recognition:
209
+ entrance = np.array(entrance[:-1]) # last pair is [im_w, im_h]
210
+
211
+ text_scale = max(0.5, image.shape[1] / 3000.)
212
+ text_thickness = 2
213
+ line_thickness = max(1, int(image.shape[1] / 500.))
214
+
215
+ if num_classes == 1:
216
+ if records is not None:
217
+ start = records[-1].find('Total')
218
+ end = records[-1].find('In')
219
+ cv2.putText(
220
+ im,
221
+ records[-1][start:end], (0, int(40 * text_scale) + 10),
222
+ cv2.FONT_ITALIC,
223
+ text_scale, (0, 0, 255),
224
+ thickness=text_thickness)
225
+
226
+ if num_classes == 1 and do_entrance_counting:
227
+ entrance_line = tuple(map(int, entrance))
228
+ cv2.rectangle(
229
+ im,
230
+ entrance_line[0:2],
231
+ entrance_line[2:4],
232
+ color=(0, 255, 255),
233
+ thickness=line_thickness)
234
+ # find start location for entrance counting data
235
+ start = records[-1].find('In')
236
+ cv2.putText(
237
+ im,
238
+ records[-1][start:-1], (0, int(60 * text_scale) + 10),
239
+ cv2.FONT_ITALIC,
240
+ text_scale, (0, 0, 255),
241
+ thickness=text_thickness)
242
+
243
+ if num_classes == 1 and (do_break_in_counting or
244
+ do_illegal_parking_recognition):
245
+ np_masks = np.zeros((im_h, im_w, 1), np.uint8)
246
+ cv2.fillPoly(np_masks, [entrance], 255)
247
+
248
+ # Draw region mask
249
+ alpha = 0.3
250
+ im = np.array(im).astype('float32')
251
+ mask = np_masks[:, :, 0]
252
+ color_mask = [0, 0, 255]
253
+ idx = np.nonzero(mask)
254
+ color_mask = np.array(color_mask)
255
+ im[idx[0], idx[1], :] *= 1.0 - alpha
256
+ im[idx[0], idx[1], :] += alpha * color_mask
257
+ im = np.array(im).astype('uint8')
258
+
259
+ if do_break_in_counting:
260
+ # find start location for break in counting data
261
+ start = records[-1].find('Break_in')
262
+ cv2.putText(
263
+ im,
264
+ records[-1][start:-1],
265
+ (entrance[0][0] - 10, entrance[0][1] - 10),
266
+ cv2.FONT_ITALIC,
267
+ text_scale, (0, 0, 255),
268
+ thickness=text_thickness)
269
+
270
+ if illegal_parking_dict is not None and len(illegal_parking_dict) != 0:
271
+ for key, value in illegal_parking_dict.items():
272
+ x1, y1, w, h = value['bbox']
273
+ plate = value['plate']
274
+ if plate is None:
275
+ plate = ""
276
+
277
+ # red box
278
+ cv2.rectangle(im, (int(x1), int(y1)),
279
+ (int(x1 + w), int(y1 + h)), (0, 0, 255), 2)
280
+
281
+ cv2.putText(
282
+ im,
283
+ "illegal_parking:" + plate,
284
+ (int(x1) + 5, int(16 * text_scale + y1 + 15)),
285
+ cv2.FONT_ITALIC,
286
+ text_scale * 1.5, (0, 0, 255),
287
+ thickness=text_thickness)
288
+
289
+ for cls_id in range(num_classes):
290
+ tlwhs = tlwhs_dict[cls_id]
291
+ obj_ids = obj_ids_dict[cls_id]
292
+ scores = scores_dict[cls_id]
293
+ cv2.putText(
294
+ im,
295
+ 'frame: %d fps: %.2f num: %d' % (frame_id, fps, len(tlwhs)),
296
+ (0, int(15 * text_scale) + 5),
297
+ cv2.FONT_ITALIC,
298
+ text_scale, (0, 0, 255),
299
+ thickness=text_thickness)
300
+
301
+ record_id = set()
302
+ for i, tlwh in enumerate(tlwhs):
303
+ x1, y1, w, h = tlwh
304
+ intbox = tuple(map(int, (x1, y1, x1 + w, y1 + h)))
305
+ center = tuple(map(int, (x1 + w / 2., y1 + h / 2.)))
306
+ obj_id = int(obj_ids[i])
307
+ if center_traj is not None:
308
+ record_id.add(obj_id)
309
+ if obj_id not in center_traj[cls_id]:
310
+ center_traj[cls_id][obj_id] = deque(maxlen=30)
311
+ center_traj[cls_id][obj_id].append(center)
312
+
313
+ id_text = '{}'.format(int(obj_id))
314
+ if ids2names != []:
315
+ id_text = '{}_{}'.format(ids2names[cls_id], id_text)
316
+ else:
317
+ id_text = 'class{}_{}'.format(cls_id, id_text)
318
+
319
+ _line_thickness = 1 if obj_id <= 0 else line_thickness
320
+
321
+ in_region = False
322
+ if do_break_in_counting:
323
+ center_x = min(x1 + w / 2., im_w - 1)
324
+ center_down_y = min(y1 + h, im_h - 1)
325
+ if in_quadrangle([center_x, center_down_y], entrance, im_h,
326
+ im_w):
327
+ in_region = True
328
+
329
+ color = get_color(abs(obj_id)) if in_region == False else (0, 0,
330
+ 255)
331
+ cv2.rectangle(
332
+ im,
333
+ intbox[0:2],
334
+ intbox[2:4],
335
+ color=color,
336
+ thickness=line_thickness)
337
+ cv2.putText(
338
+ im,
339
+ id_text, (intbox[0], intbox[1] - 25),
340
+ cv2.FONT_ITALIC,
341
+ text_scale,
342
+ color,
343
+ thickness=text_thickness)
344
+
345
+ if do_break_in_counting and in_region:
346
+ cv2.putText(
347
+ im,
348
+ 'Break in now.', (intbox[0], intbox[1] - 50),
349
+ cv2.FONT_ITALIC,
350
+ text_scale, (0, 0, 255),
351
+ thickness=text_thickness)
352
+
353
+ if scores is not None:
354
+ text = 'score: {:.2f}'.format(float(scores[i]))
355
+ cv2.putText(
356
+ im,
357
+ text, (intbox[0], intbox[1] - 6),
358
+ cv2.FONT_ITALIC,
359
+ text_scale,
360
+ color,
361
+ thickness=text_thickness)
362
+ if center_traj is not None:
363
+ for traj in center_traj:
364
+ for i in traj.keys():
365
+ if i not in record_id:
366
+ continue
367
+ for point in traj[i]:
368
+ cv2.circle(im, point, 3, (0, 0, 255), -1)
369
+ return im
370
+
371
+
372
+ def in_quadrangle(point, entrance, im_h, im_w):
373
+ mask = np.zeros((im_h, im_w, 1), np.uint8)
374
+ cv2.fillPoly(mask, [entrance], 255)
375
+ p = tuple(map(int, point))
376
+ if mask[p[1], p[0], :] > 0:
377
+ return True
378
+ else:
379
+ return False
pptracking/python/mot_jde_infer.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import time
17
+ import yaml
18
+ import cv2
19
+ import numpy as np
20
+ from collections import defaultdict
21
+ import paddle
22
+
23
+ from benchmark_utils import PaddleInferBenchmark
24
+ from preprocess import decode_image
25
+ from mot_utils import argsparser, Timer, get_current_memory_mb
26
+ from det_infer import Detector, get_test_images, print_arguments, bench_log, PredictConfig
27
+
28
+ # add python path
29
+ import sys
30
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2)))
31
+ sys.path.insert(0, parent_path)
32
+
33
+ from mot import JDETracker
34
+ from mot.utils import MOTTimer, write_mot_results, flow_statistic
35
+ from mot.visualize import plot_tracking, plot_tracking_dict
36
+
37
+ # Global dictionary
38
+ MOT_JDE_SUPPORT_MODELS = {
39
+ 'JDE',
40
+ 'FairMOT',
41
+ }
42
+
43
+
44
+ class JDE_Detector(Detector):
45
+ """
46
+ Args:
47
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
48
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
49
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
50
+ batch_size (int): size of pre batch in inference
51
+ trt_min_shape (int): min shape for dynamic shape in trt
52
+ trt_max_shape (int): max shape for dynamic shape in trt
53
+ trt_opt_shape (int): opt shape for dynamic shape in trt
54
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
55
+ calibration, trt_calib_mode need to set True
56
+ cpu_threads (int): cpu threads
57
+ enable_mkldnn (bool): whether to open MKLDNN
58
+ output_dir (string): The path of output, default as 'output'
59
+ threshold (float): Score threshold of the detected bbox, default as 0.5
60
+ save_images (bool): Whether to save visualization image results, default as False
61
+ save_mot_txts (bool): Whether to save tracking results (txt), default as False
62
+ draw_center_traj (bool): Whether drawing the trajectory of center, default as False
63
+ secs_interval (int): The seconds interval to count after tracking, default as 10
64
+ skip_frame_num (int): Skip frame num to get faster MOT results, default as -1
65
+ do_entrance_counting(bool): Whether counting the numbers of identifiers entering
66
+ or getting out from the entrance, default as False,only support single class
67
+ counting in MOT.
68
+ do_break_in_counting(bool): Whether counting the numbers of identifiers break in
69
+ the area, default as False,only support single class counting in MOT,
70
+ and the video should be taken by a static camera.
71
+ region_type (str): Area type for entrance counting or break in counting, 'horizontal'
72
+ and 'vertical' used when do entrance counting. 'custom' used when do break in counting.
73
+ Note that only support single-class MOT, and the video should be taken by a static camera.
74
+ region_polygon (list): Clockwise point coords (x0,y0,x1,y1...) of polygon of area when
75
+ do_break_in_counting. Note that only support single-class MOT and
76
+ the video should be taken by a static camera.
77
+ """
78
+
79
+ def __init__(self,
80
+ model_dir,
81
+ tracker_config=None,
82
+ device='CPU',
83
+ run_mode='paddle',
84
+ batch_size=1,
85
+ trt_min_shape=1,
86
+ trt_max_shape=1088,
87
+ trt_opt_shape=608,
88
+ trt_calib_mode=False,
89
+ cpu_threads=1,
90
+ enable_mkldnn=False,
91
+ output_dir='output',
92
+ threshold=0.5,
93
+ save_images=False,
94
+ save_mot_txts=False,
95
+ draw_center_traj=False,
96
+ secs_interval=10,
97
+ skip_frame_num=-1,
98
+ do_entrance_counting=False,
99
+ do_break_in_counting=False,
100
+ region_type='horizontal',
101
+ region_polygon=[]):
102
+ super(JDE_Detector, self).__init__(
103
+ model_dir=model_dir,
104
+ device=device,
105
+ run_mode=run_mode,
106
+ batch_size=batch_size,
107
+ trt_min_shape=trt_min_shape,
108
+ trt_max_shape=trt_max_shape,
109
+ trt_opt_shape=trt_opt_shape,
110
+ trt_calib_mode=trt_calib_mode,
111
+ cpu_threads=cpu_threads,
112
+ enable_mkldnn=enable_mkldnn,
113
+ output_dir=output_dir,
114
+ threshold=threshold, )
115
+ self.save_images = save_images
116
+ self.save_mot_txts = save_mot_txts
117
+ self.draw_center_traj = draw_center_traj
118
+ self.secs_interval = secs_interval
119
+ self.skip_frame_num = skip_frame_num
120
+ self.do_entrance_counting = do_entrance_counting
121
+ self.do_break_in_counting = do_break_in_counting
122
+ self.region_type = region_type
123
+ self.region_polygon = region_polygon
124
+ if self.region_type == 'custom':
125
+ assert len(
126
+ self.region_polygon
127
+ ) > 6, 'region_type is custom, region_polygon should be at least 3 pairs of point coords.'
128
+
129
+ assert batch_size == 1, "MOT model only supports batch_size=1."
130
+ self.det_times = Timer(with_tracker=True)
131
+ self.num_classes = len(self.pred_config.labels)
132
+ if self.skip_frame_num > 1:
133
+ self.previous_det_result = None
134
+
135
+ # tracker config
136
+ assert self.pred_config.tracker, "The exported JDE Detector model should have tracker."
137
+ cfg = self.pred_config.tracker
138
+ min_box_area = cfg.get('min_box_area', 0.0)
139
+ vertical_ratio = cfg.get('vertical_ratio', 0.0)
140
+ conf_thres = cfg.get('conf_thres', 0.0)
141
+ tracked_thresh = cfg.get('tracked_thresh', 0.7)
142
+ metric_type = cfg.get('metric_type', 'euclidean')
143
+
144
+ self.tracker = JDETracker(
145
+ num_classes=self.num_classes,
146
+ min_box_area=min_box_area,
147
+ vertical_ratio=vertical_ratio,
148
+ conf_thres=conf_thres,
149
+ tracked_thresh=tracked_thresh,
150
+ metric_type=metric_type)
151
+
152
+ def postprocess(self, inputs, result):
153
+ # postprocess output of predictor
154
+ np_boxes = result['pred_dets']
155
+ if np_boxes.shape[0] <= 0:
156
+ print('[WARNNING] No object detected.')
157
+ result = {'pred_dets': np.zeros([0, 6]), 'pred_embs': None}
158
+ result = {k: v for k, v in result.items() if v is not None}
159
+ return result
160
+
161
+ def tracking(self, det_results):
162
+ pred_dets = det_results['pred_dets'] # cls_id, score, x0, y0, x1, y1
163
+ pred_embs = det_results['pred_embs']
164
+ online_targets_dict = self.tracker.update(pred_dets, pred_embs)
165
+
166
+ online_tlwhs = defaultdict(list)
167
+ online_scores = defaultdict(list)
168
+ online_ids = defaultdict(list)
169
+ for cls_id in range(self.num_classes):
170
+ online_targets = online_targets_dict[cls_id]
171
+ for t in online_targets:
172
+ tlwh = t.tlwh
173
+ tid = t.track_id
174
+ tscore = t.score
175
+ if tlwh[2] * tlwh[3] <= self.tracker.min_box_area: continue
176
+ if self.tracker.vertical_ratio > 0 and tlwh[2] / tlwh[
177
+ 3] > self.tracker.vertical_ratio:
178
+ continue
179
+ online_tlwhs[cls_id].append(tlwh)
180
+ online_ids[cls_id].append(tid)
181
+ online_scores[cls_id].append(tscore)
182
+ return online_tlwhs, online_scores, online_ids
183
+
184
+ def predict(self, repeats=1):
185
+ '''
186
+ Args:
187
+ repeats (int): repeats number for prediction
188
+ Returns:
189
+ result (dict): include 'pred_dets': np.ndarray: shape:[N,6], N: number of box,
190
+ matix element:[class, score, x_min, y_min, x_max, y_max]
191
+ FairMOT(JDE)'s result include 'pred_embs': np.ndarray:
192
+ shape: [N, 128]
193
+ '''
194
+ # model prediction
195
+ np_pred_dets, np_pred_embs = None, None
196
+ for i in range(repeats):
197
+ self.predictor.run()
198
+ output_names = self.predictor.get_output_names()
199
+ boxes_tensor = self.predictor.get_output_handle(output_names[0])
200
+ np_pred_dets = boxes_tensor.copy_to_cpu()
201
+ embs_tensor = self.predictor.get_output_handle(output_names[1])
202
+ np_pred_embs = embs_tensor.copy_to_cpu()
203
+
204
+ result = dict(pred_dets=np_pred_dets, pred_embs=np_pred_embs)
205
+ return result
206
+
207
+ def predict_image(self,
208
+ image_list,
209
+ run_benchmark=False,
210
+ repeats=1,
211
+ visual=True,
212
+ seq_name=None,
213
+ reuse_det_result=False):
214
+ mot_results = []
215
+ num_classes = self.num_classes
216
+ image_list.sort()
217
+ ids2names = self.pred_config.labels
218
+ data_type = 'mcmot' if num_classes > 1 else 'mot'
219
+ for frame_id, img_file in enumerate(image_list):
220
+ batch_image_list = [img_file] # bs=1 in MOT model
221
+ if run_benchmark:
222
+ # preprocess
223
+ inputs = self.preprocess(batch_image_list) # warmup
224
+ self.det_times.preprocess_time_s.start()
225
+ inputs = self.preprocess(batch_image_list)
226
+ self.det_times.preprocess_time_s.end()
227
+
228
+ # model prediction
229
+ result_warmup = self.predict(repeats=repeats) # warmup
230
+ self.det_times.inference_time_s.start()
231
+ result = self.predict(repeats=repeats)
232
+ self.det_times.inference_time_s.end(repeats=repeats)
233
+
234
+ # postprocess
235
+ result_warmup = self.postprocess(inputs, result) # warmup
236
+ self.det_times.postprocess_time_s.start()
237
+ det_result = self.postprocess(inputs, result)
238
+ self.det_times.postprocess_time_s.end()
239
+
240
+ # tracking
241
+ result_warmup = self.tracking(det_result)
242
+ self.det_times.tracking_time_s.start()
243
+ online_tlwhs, online_scores, online_ids = self.tracking(
244
+ det_result)
245
+ self.det_times.tracking_time_s.end()
246
+ self.det_times.img_num += 1
247
+
248
+ cm, gm, gu = get_current_memory_mb()
249
+ self.cpu_mem += cm
250
+ self.gpu_mem += gm
251
+ self.gpu_util += gu
252
+
253
+ else:
254
+ self.det_times.preprocess_time_s.start()
255
+ if not reuse_det_result:
256
+ inputs = self.preprocess(batch_image_list)
257
+ self.det_times.preprocess_time_s.end()
258
+
259
+ self.det_times.inference_time_s.start()
260
+ if not reuse_det_result:
261
+ result = self.predict()
262
+ self.det_times.inference_time_s.end()
263
+
264
+ self.det_times.postprocess_time_s.start()
265
+ if not reuse_det_result:
266
+ det_result = self.postprocess(inputs, result)
267
+ self.previous_det_result = det_result
268
+ else:
269
+ assert self.previous_det_result is not None
270
+ det_result = self.previous_det_result
271
+ self.det_times.postprocess_time_s.end()
272
+
273
+ # tracking process
274
+ self.det_times.tracking_time_s.start()
275
+ online_tlwhs, online_scores, online_ids = self.tracking(
276
+ det_result)
277
+ self.det_times.tracking_time_s.end()
278
+ self.det_times.img_num += 1
279
+
280
+ if visual:
281
+ if len(image_list) > 1 and frame_id % 10 == 0:
282
+ print('Tracking frame {}'.format(frame_id))
283
+ frame, _ = decode_image(img_file, {})
284
+
285
+ im = plot_tracking_dict(
286
+ frame,
287
+ num_classes,
288
+ online_tlwhs,
289
+ online_ids,
290
+ online_scores,
291
+ frame_id=frame_id,
292
+ ids2names=ids2names)
293
+ if seq_name is None:
294
+ seq_name = image_list[0].split('/')[-2]
295
+ save_dir = os.path.join(self.output_dir, seq_name)
296
+ if not os.path.exists(save_dir):
297
+ os.makedirs(save_dir)
298
+ cv2.imwrite(
299
+ os.path.join(save_dir, '{:05d}.jpg'.format(frame_id)), im)
300
+
301
+ mot_results.append([online_tlwhs, online_scores, online_ids])
302
+ return mot_results
303
+
304
+ def predict_video(self, video_file, camera_id):
305
+ video_out_name = 'mot_output.mp4'
306
+ if camera_id != -1:
307
+ capture = cv2.VideoCapture(camera_id)
308
+ else:
309
+ capture = cv2.VideoCapture(video_file)
310
+ video_out_name = os.path.split(video_file)[-1]
311
+ # Get Video info : resolution, fps, frame count
312
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
313
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
314
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
315
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
316
+ print("fps: %d, frame_count: %d" % (fps, frame_count))
317
+
318
+ if not os.path.exists(self.output_dir):
319
+ os.makedirs(self.output_dir)
320
+ out_path = os.path.join(self.output_dir, video_out_name)
321
+ video_format = 'mp4v'
322
+ fourcc = cv2.VideoWriter_fourcc(*video_format)
323
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
324
+
325
+ frame_id = 0
326
+ timer = MOTTimer()
327
+ results = defaultdict(list) # support single class and multi classes
328
+ num_classes = self.num_classes
329
+ data_type = 'mcmot' if num_classes > 1 else 'mot'
330
+ ids2names = self.pred_config.labels
331
+
332
+ center_traj = None
333
+ entrance = None
334
+ records = None
335
+ if self.draw_center_traj:
336
+ center_traj = [{} for i in range(num_classes)]
337
+ if num_classes == 1:
338
+ id_set = set()
339
+ interval_id_set = set()
340
+ in_id_list = list()
341
+ out_id_list = list()
342
+ prev_center = dict()
343
+ records = list()
344
+ if self.do_entrance_counting or self.do_break_in_counting:
345
+ if self.region_type == 'horizontal':
346
+ entrance = [0, height / 2., width, height / 2.]
347
+ elif self.region_type == 'vertical':
348
+ entrance = [width / 2, 0., width / 2, height]
349
+ elif self.region_type == 'custom':
350
+ entrance = []
351
+ assert len(
352
+ self.region_polygon
353
+ ) % 2 == 0, "region_polygon should be pairs of coords points when do break_in counting."
354
+ for i in range(0, len(self.region_polygon), 2):
355
+ entrance.append([
356
+ self.region_polygon[i], self.region_polygon[i + 1]
357
+ ])
358
+ entrance.append([width, height])
359
+ else:
360
+ raise ValueError("region_type:{} is not supported.".format(
361
+ self.region_type))
362
+
363
+ video_fps = fps
364
+
365
+ while (1):
366
+ ret, frame = capture.read()
367
+ if not ret:
368
+ break
369
+ if frame_id % 10 == 0:
370
+ print('Tracking frame: %d' % (frame_id))
371
+
372
+ timer.tic()
373
+ mot_skip_frame_num = self.skip_frame_num
374
+ reuse_det_result = False
375
+ if mot_skip_frame_num > 1 and frame_id > 0 and frame_id % mot_skip_frame_num > 0:
376
+ reuse_det_result = True
377
+ seq_name = video_out_name.split('.')[0]
378
+ mot_results = self.predict_image(
379
+ [frame],
380
+ visual=False,
381
+ seq_name=seq_name,
382
+ reuse_det_result=reuse_det_result)
383
+ timer.toc()
384
+
385
+ online_tlwhs, online_scores, online_ids = mot_results[0]
386
+ for cls_id in range(num_classes):
387
+ results[cls_id].append(
388
+ (frame_id + 1, online_tlwhs[cls_id], online_scores[cls_id],
389
+ online_ids[cls_id]))
390
+
391
+ # NOTE: just implement flow statistic for single class
392
+ if num_classes == 1:
393
+ result = (frame_id + 1, online_tlwhs[0], online_scores[0],
394
+ online_ids[0])
395
+ statistic = flow_statistic(
396
+ result,
397
+ self.secs_interval,
398
+ self.do_entrance_counting,
399
+ self.do_break_in_counting,
400
+ self.region_type,
401
+ video_fps,
402
+ entrance,
403
+ id_set,
404
+ interval_id_set,
405
+ in_id_list,
406
+ out_id_list,
407
+ prev_center,
408
+ records,
409
+ data_type,
410
+ ids2names=self.pred_config.labels)
411
+ records = statistic['records']
412
+
413
+ fps = 1. / timer.duration
414
+ im = plot_tracking_dict(
415
+ frame,
416
+ num_classes,
417
+ online_tlwhs,
418
+ online_ids,
419
+ online_scores,
420
+ frame_id=frame_id,
421
+ fps=fps,
422
+ ids2names=ids2names,
423
+ do_entrance_counting=self.do_entrance_counting,
424
+ entrance=entrance,
425
+ records=records,
426
+ center_traj=center_traj)
427
+
428
+ writer.write(im)
429
+ if camera_id != -1:
430
+ cv2.imshow('Mask Detection', im)
431
+ if cv2.waitKey(1) & 0xFF == ord('q'):
432
+ break
433
+ frame_id += 1
434
+
435
+ if self.save_mot_txts:
436
+ result_filename = os.path.join(
437
+ self.output_dir, video_out_name.split('.')[-2] + '.txt')
438
+
439
+ write_mot_results(result_filename, results, data_type, num_classes)
440
+
441
+ if num_classes == 1:
442
+ result_filename = os.path.join(
443
+ self.output_dir,
444
+ video_out_name.split('.')[-2] + '_flow_statistic.txt')
445
+ f = open(result_filename, 'w')
446
+ for line in records:
447
+ f.write(line)
448
+ print('Flow statistic save in {}'.format(result_filename))
449
+ f.close()
450
+
451
+ writer.release()
452
+
453
+
454
+ def main():
455
+ detector = JDE_Detector(
456
+ FLAGS.model_dir,
457
+ tracker_config=None,
458
+ device=FLAGS.device,
459
+ run_mode=FLAGS.run_mode,
460
+ batch_size=1,
461
+ trt_min_shape=FLAGS.trt_min_shape,
462
+ trt_max_shape=FLAGS.trt_max_shape,
463
+ trt_opt_shape=FLAGS.trt_opt_shape,
464
+ trt_calib_mode=FLAGS.trt_calib_mode,
465
+ cpu_threads=FLAGS.cpu_threads,
466
+ enable_mkldnn=FLAGS.enable_mkldnn,
467
+ output_dir=FLAGS.output_dir,
468
+ threshold=FLAGS.threshold,
469
+ save_images=FLAGS.save_images,
470
+ save_mot_txts=FLAGS.save_mot_txts,
471
+ draw_center_traj=FLAGS.draw_center_traj,
472
+ secs_interval=FLAGS.secs_interval,
473
+ skip_frame_num=FLAGS.skip_frame_num,
474
+ do_entrance_counting=FLAGS.do_entrance_counting,
475
+ do_break_in_counting=FLAGS.do_break_in_counting,
476
+ region_type=FLAGS.region_type,
477
+ region_polygon=FLAGS.region_polygon)
478
+
479
+ # predict from video file or camera video stream
480
+ if FLAGS.video_file is not None or FLAGS.camera_id != -1:
481
+ detector.predict_video(FLAGS.video_file, FLAGS.camera_id)
482
+ else:
483
+ # predict from image
484
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
485
+ detector.predict_image(img_list, FLAGS.run_benchmark, repeats=10)
486
+
487
+ if not FLAGS.run_benchmark:
488
+ detector.det_times.info(average=True)
489
+ else:
490
+ mode = FLAGS.run_mode
491
+ model_dir = FLAGS.model_dir
492
+ model_info = {
493
+ 'model_name': model_dir.strip('/').split('/')[-1],
494
+ 'precision': mode.split('_')[-1]
495
+ }
496
+ bench_log(detector, img_list, model_info, name='MOT')
497
+
498
+
499
+ if __name__ == '__main__':
500
+ paddle.enable_static()
501
+ parser = argsparser()
502
+ FLAGS = parser.parse_args()
503
+ print_arguments(FLAGS)
504
+ FLAGS.device = FLAGS.device.upper()
505
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
506
+ ], "device should be CPU, GPU or XPU"
507
+
508
+ main()
pptracking/python/mot_sde_infer.py ADDED
@@ -0,0 +1,882 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import time
17
+ import yaml
18
+ import cv2
19
+ import re
20
+ import glob
21
+ import numpy as np
22
+ from collections import defaultdict
23
+ import paddle
24
+
25
+ from benchmark_utils import PaddleInferBenchmark
26
+ from preprocess import decode_image
27
+
28
+ # add python path
29
+ import sys
30
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'])))
31
+ sys.path.insert(0, parent_path)
32
+
33
+ from det_infer import Detector, get_test_images, print_arguments, bench_log, PredictConfig, load_predictor
34
+ from mot_utils import argsparser, Timer, get_current_memory_mb, video2frames, _is_valid_video
35
+ from mot.tracker import JDETracker, DeepSORTTracker, OCSORTTracker
36
+ from mot.utils import MOTTimer, write_mot_results, get_crops, clip_box, flow_statistic
37
+ from mot.visualize import plot_tracking, plot_tracking_dict
38
+
39
+ from mot.mtmct.utils import parse_bias
40
+ from mot.mtmct.postprocess import trajectory_fusion, sub_cluster, gen_res, print_mtmct_result
41
+ from mot.mtmct.postprocess import get_mtmct_matching_results, save_mtmct_crops, save_mtmct_vis_results
42
+
43
+
44
+ class SDE_Detector(Detector):
45
+ """
46
+ Args:
47
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
48
+ tracker_config (str): tracker config path
49
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
50
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
51
+ batch_size (int): size of pre batch in inference
52
+ trt_min_shape (int): min shape for dynamic shape in trt
53
+ trt_max_shape (int): max shape for dynamic shape in trt
54
+ trt_opt_shape (int): opt shape for dynamic shape in trt
55
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
56
+ calibration, trt_calib_mode need to set True
57
+ cpu_threads (int): cpu threads
58
+ enable_mkldnn (bool): whether to open MKLDNN
59
+ output_dir (string): The path of output, default as 'output'
60
+ threshold (float): Score threshold of the detected bbox, default as 0.5
61
+ save_images (bool): Whether to save visualization image results, default as False
62
+ save_mot_txts (bool): Whether to save tracking results (txt), default as False
63
+ draw_center_traj (bool): Whether drawing the trajectory of center, default as False
64
+ secs_interval (int): The seconds interval to count after tracking, default as 10
65
+ skip_frame_num (int): Skip frame num to get faster MOT results, default as -1
66
+ do_entrance_counting(bool): Whether counting the numbers of identifiers entering
67
+ or getting out from the entrance, default as False,only support single class
68
+ counting in MOT, and the video should be taken by a static camera.
69
+ do_break_in_counting(bool): Whether counting the numbers of identifiers break in
70
+ the area, default as False,only support single class counting in MOT,
71
+ and the video should be taken by a static camera.
72
+ region_type (str): Area type for entrance counting or break in counting, 'horizontal'
73
+ and 'vertical' used when do entrance counting. 'custom' used when do break in counting.
74
+ Note that only support single-class MOT, and the video should be taken by a static camera.
75
+ region_polygon (list): Clockwise point coords (x0,y0,x1,y1...) of polygon of area when
76
+ do_break_in_counting. Note that only support single-class MOT and
77
+ the video should be taken by a static camera.
78
+ reid_model_dir (str): reid model dir, default None for ByteTrack, but set for DeepSORT
79
+ mtmct_dir (str): MTMCT dir, default None, set for doing MTMCT
80
+ """
81
+
82
+ def __init__(self,
83
+ model_dir,
84
+ tracker_config,
85
+ device='CPU',
86
+ run_mode='paddle',
87
+ batch_size=1,
88
+ trt_min_shape=1,
89
+ trt_max_shape=1280,
90
+ trt_opt_shape=640,
91
+ trt_calib_mode=False,
92
+ cpu_threads=1,
93
+ enable_mkldnn=False,
94
+ output_dir='output',
95
+ threshold=0.5,
96
+ save_images=False,
97
+ save_mot_txts=False,
98
+ draw_center_traj=False,
99
+ secs_interval=10,
100
+ skip_frame_num=-1,
101
+ do_entrance_counting=False,
102
+ do_break_in_counting=False,
103
+ region_type='horizontal',
104
+ region_polygon=[],
105
+ reid_model_dir=None,
106
+ mtmct_dir=None):
107
+ super(SDE_Detector, self).__init__(
108
+ model_dir=model_dir,
109
+ device=device,
110
+ run_mode=run_mode,
111
+ batch_size=batch_size,
112
+ trt_min_shape=trt_min_shape,
113
+ trt_max_shape=trt_max_shape,
114
+ trt_opt_shape=trt_opt_shape,
115
+ trt_calib_mode=trt_calib_mode,
116
+ cpu_threads=cpu_threads,
117
+ enable_mkldnn=enable_mkldnn,
118
+ output_dir=output_dir,
119
+ threshold=threshold, )
120
+ self.save_images = save_images
121
+ self.save_mot_txts = save_mot_txts
122
+ self.draw_center_traj = draw_center_traj
123
+ self.secs_interval = secs_interval
124
+ self.skip_frame_num = skip_frame_num
125
+ self.do_entrance_counting = do_entrance_counting
126
+ self.do_break_in_counting = do_break_in_counting
127
+ self.region_type = region_type
128
+ self.region_polygon = region_polygon
129
+ if self.region_type == 'custom':
130
+ assert len(
131
+ self.region_polygon
132
+ ) > 6, 'region_type is custom, region_polygon should be at least 3 pairs of point coords.'
133
+
134
+ assert batch_size == 1, "MOT model only supports batch_size=1."
135
+ self.det_times = Timer(with_tracker=True)
136
+ self.num_classes = len(self.pred_config.labels)
137
+ if self.skip_frame_num > 1:
138
+ self.previous_det_result = None
139
+
140
+ # reid config
141
+ self.use_reid = False if reid_model_dir is None else True
142
+ if self.use_reid:
143
+ self.reid_pred_config = self.set_config(reid_model_dir)
144
+ self.reid_predictor, self.config = load_predictor(
145
+ reid_model_dir,
146
+ run_mode=run_mode,
147
+ batch_size=50, # reid_batch_size
148
+ min_subgraph_size=self.reid_pred_config.min_subgraph_size,
149
+ device=device,
150
+ use_dynamic_shape=self.reid_pred_config.use_dynamic_shape,
151
+ trt_min_shape=trt_min_shape,
152
+ trt_max_shape=trt_max_shape,
153
+ trt_opt_shape=trt_opt_shape,
154
+ trt_calib_mode=trt_calib_mode,
155
+ cpu_threads=cpu_threads,
156
+ enable_mkldnn=enable_mkldnn)
157
+ else:
158
+ self.reid_pred_config = None
159
+ self.reid_predictor = None
160
+
161
+ assert tracker_config is not None, 'Note that tracker_config should be set.'
162
+ self.tracker_config = tracker_config
163
+ tracker_cfg = yaml.safe_load(open(self.tracker_config))
164
+ cfg = tracker_cfg[tracker_cfg['type']]
165
+
166
+ # tracker config
167
+ self.use_deepsort_tracker = True if tracker_cfg[
168
+ 'type'] == 'DeepSORTTracker' else False
169
+ self.use_ocsort_tracker = True if tracker_cfg[
170
+ 'type'] == 'OCSORTTracker' else False
171
+
172
+ if self.use_deepsort_tracker:
173
+ if self.reid_pred_config is not None and hasattr(
174
+ self.reid_pred_config, 'tracker'):
175
+ cfg = self.reid_pred_config.tracker
176
+ budget = cfg.get('budget', 100)
177
+ max_age = cfg.get('max_age', 30)
178
+ max_iou_distance = cfg.get('max_iou_distance', 0.7)
179
+ matching_threshold = cfg.get('matching_threshold', 0.2)
180
+ min_box_area = cfg.get('min_box_area', 0)
181
+ vertical_ratio = cfg.get('vertical_ratio', 0)
182
+
183
+ self.tracker = DeepSORTTracker(
184
+ budget=budget,
185
+ max_age=max_age,
186
+ max_iou_distance=max_iou_distance,
187
+ matching_threshold=matching_threshold,
188
+ min_box_area=min_box_area,
189
+ vertical_ratio=vertical_ratio, )
190
+
191
+ elif self.use_ocsort_tracker:
192
+ det_thresh = cfg.get('det_thresh', 0.4)
193
+ max_age = cfg.get('max_age', 30)
194
+ min_hits = cfg.get('min_hits', 3)
195
+ iou_threshold = cfg.get('iou_threshold', 0.3)
196
+ delta_t = cfg.get('delta_t', 3)
197
+ inertia = cfg.get('inertia', 0.2)
198
+ min_box_area = cfg.get('min_box_area', 0)
199
+ vertical_ratio = cfg.get('vertical_ratio', 0)
200
+ use_byte = cfg.get('use_byte', False)
201
+
202
+ self.tracker = OCSORTTracker(
203
+ det_thresh=det_thresh,
204
+ max_age=max_age,
205
+ min_hits=min_hits,
206
+ iou_threshold=iou_threshold,
207
+ delta_t=delta_t,
208
+ inertia=inertia,
209
+ min_box_area=min_box_area,
210
+ vertical_ratio=vertical_ratio,
211
+ use_byte=use_byte)
212
+ else:
213
+ # use ByteTracker
214
+ use_byte = cfg.get('use_byte', False)
215
+ det_thresh = cfg.get('det_thresh', 0.3)
216
+ min_box_area = cfg.get('min_box_area', 0)
217
+ vertical_ratio = cfg.get('vertical_ratio', 0)
218
+ match_thres = cfg.get('match_thres', 0.9)
219
+ conf_thres = cfg.get('conf_thres', 0.6)
220
+ low_conf_thres = cfg.get('low_conf_thres', 0.1)
221
+
222
+ self.tracker = JDETracker(
223
+ use_byte=use_byte,
224
+ det_thresh=det_thresh,
225
+ num_classes=self.num_classes,
226
+ min_box_area=min_box_area,
227
+ vertical_ratio=vertical_ratio,
228
+ match_thres=match_thres,
229
+ conf_thres=conf_thres,
230
+ low_conf_thres=low_conf_thres, )
231
+
232
+ self.do_mtmct = False if mtmct_dir is None else True
233
+ self.mtmct_dir = mtmct_dir
234
+
235
+ def postprocess(self, inputs, result):
236
+ # postprocess output of predictor
237
+ keep_idx = result['boxes'][:, 1] > self.threshold
238
+ result['boxes'] = result['boxes'][keep_idx]
239
+ np_boxes_num = [len(result['boxes'])]
240
+ if np_boxes_num[0] <= 0:
241
+ print('[WARNNING] No object detected.')
242
+ result = {'boxes': np.zeros([0, 6]), 'boxes_num': [0]}
243
+ result = {k: v for k, v in result.items() if v is not None}
244
+ return result
245
+
246
+ def reidprocess(self, det_results, repeats=1):
247
+ pred_dets = det_results['boxes'] # cls_id, score, x0, y0, x1, y1
248
+ pred_xyxys = pred_dets[:, 2:6]
249
+
250
+ ori_image = det_results['ori_image']
251
+ ori_image_shape = ori_image.shape[:2]
252
+ pred_xyxys, keep_idx = clip_box(pred_xyxys, ori_image_shape)
253
+
254
+ if len(keep_idx[0]) == 0:
255
+ det_results['boxes'] = np.zeros((1, 6), dtype=np.float32)
256
+ det_results['embeddings'] = None
257
+ return det_results
258
+
259
+ pred_dets = pred_dets[keep_idx[0]]
260
+ pred_xyxys = pred_dets[:, 2:6]
261
+
262
+ w, h = self.tracker.input_size
263
+ crops = get_crops(pred_xyxys, ori_image, w, h)
264
+
265
+ # to keep fast speed, only use topk crops
266
+ crops = crops[:50] # reid_batch_size
267
+ det_results['crops'] = np.array(crops).astype('float32')
268
+ det_results['boxes'] = pred_dets[:50]
269
+
270
+ input_names = self.reid_predictor.get_input_names()
271
+ for i in range(len(input_names)):
272
+ input_tensor = self.reid_predictor.get_input_handle(input_names[i])
273
+ input_tensor.copy_from_cpu(det_results[input_names[i]])
274
+
275
+ # model prediction
276
+ for i in range(repeats):
277
+ self.reid_predictor.run()
278
+ output_names = self.reid_predictor.get_output_names()
279
+ feature_tensor = self.reid_predictor.get_output_handle(
280
+ output_names[0])
281
+ pred_embs = feature_tensor.copy_to_cpu()
282
+
283
+ det_results['embeddings'] = pred_embs
284
+ return det_results
285
+
286
+ def tracking(self, det_results):
287
+ pred_dets = det_results['boxes'] # cls_id, score, x0, y0, x1, y1
288
+ pred_embs = det_results.get('embeddings', None)
289
+
290
+ if self.use_deepsort_tracker:
291
+ # use DeepSORTTracker, only support singe class
292
+ self.tracker.predict()
293
+ online_targets = self.tracker.update(pred_dets, pred_embs)
294
+ online_tlwhs, online_scores, online_ids = [], [], []
295
+ if self.do_mtmct:
296
+ online_tlbrs, online_feats = [], []
297
+ for t in online_targets:
298
+ if not t.is_confirmed() or t.time_since_update > 1:
299
+ continue
300
+ tlwh = t.to_tlwh()
301
+ tscore = t.score
302
+ tid = t.track_id
303
+ if self.tracker.vertical_ratio > 0 and tlwh[2] / tlwh[
304
+ 3] > self.tracker.vertical_ratio:
305
+ continue
306
+ online_tlwhs.append(tlwh)
307
+ online_scores.append(tscore)
308
+ online_ids.append(tid)
309
+ if self.do_mtmct:
310
+ online_tlbrs.append(t.to_tlbr())
311
+ online_feats.append(t.feat)
312
+
313
+ tracking_outs = {
314
+ 'online_tlwhs': online_tlwhs,
315
+ 'online_scores': online_scores,
316
+ 'online_ids': online_ids,
317
+ }
318
+ if self.do_mtmct:
319
+ seq_name = det_results['seq_name']
320
+ frame_id = det_results['frame_id']
321
+
322
+ tracking_outs['feat_data'] = {}
323
+ for _tlbr, _id, _feat in zip(online_tlbrs, online_ids,
324
+ online_feats):
325
+ feat_data = {}
326
+ feat_data['bbox'] = _tlbr
327
+ feat_data['frame'] = f"{frame_id:06d}"
328
+ feat_data['id'] = _id
329
+ _imgname = f'{seq_name}_{_id}_{frame_id}.jpg'
330
+ feat_data['imgname'] = _imgname
331
+ feat_data['feat'] = _feat
332
+ tracking_outs['feat_data'].update({_imgname: feat_data})
333
+ return tracking_outs
334
+
335
+ elif self.use_ocsort_tracker:
336
+ # use OCSORTTracker, only support singe class
337
+ online_targets = self.tracker.update(pred_dets, pred_embs)
338
+ online_tlwhs = defaultdict(list)
339
+ online_scores = defaultdict(list)
340
+ online_ids = defaultdict(list)
341
+ for t in online_targets:
342
+ tlwh = [t[0], t[1], t[2] - t[0], t[3] - t[1]]
343
+ tscore = float(t[4])
344
+ tid = int(t[5])
345
+ if tlwh[2] * tlwh[3] <= self.tracker.min_box_area: continue
346
+ if self.tracker.vertical_ratio > 0 and tlwh[2] / tlwh[
347
+ 3] > self.tracker.vertical_ratio:
348
+ continue
349
+ if tlwh[2] * tlwh[3] > 0:
350
+ online_tlwhs[0].append(tlwh)
351
+ online_ids[0].append(tid)
352
+ online_scores[0].append(tscore)
353
+ tracking_outs = {
354
+ 'online_tlwhs': online_tlwhs,
355
+ 'online_scores': online_scores,
356
+ 'online_ids': online_ids,
357
+ }
358
+ return tracking_outs
359
+
360
+ else:
361
+ # use ByteTracker, support multiple class
362
+ online_tlwhs = defaultdict(list)
363
+ online_scores = defaultdict(list)
364
+ online_ids = defaultdict(list)
365
+ if self.do_mtmct:
366
+ online_tlbrs, online_feats = defaultdict(list), defaultdict(
367
+ list)
368
+ online_targets_dict = self.tracker.update(pred_dets, pred_embs)
369
+ for cls_id in range(self.num_classes):
370
+ online_targets = online_targets_dict[cls_id]
371
+ for t in online_targets:
372
+ tlwh = t.tlwh
373
+ tid = t.track_id
374
+ tscore = t.score
375
+ if tlwh[2] * tlwh[3] <= self.tracker.min_box_area:
376
+ continue
377
+ if self.tracker.vertical_ratio > 0 and tlwh[2] / tlwh[
378
+ 3] > self.tracker.vertical_ratio:
379
+ continue
380
+ online_tlwhs[cls_id].append(tlwh)
381
+ online_ids[cls_id].append(tid)
382
+ online_scores[cls_id].append(tscore)
383
+ if self.do_mtmct:
384
+ online_tlbrs[cls_id].append(t.tlbr)
385
+ online_feats[cls_id].append(t.curr_feat)
386
+
387
+ if self.do_mtmct:
388
+ assert self.num_classes == 1, 'MTMCT only support single class.'
389
+ tracking_outs = {
390
+ 'online_tlwhs': online_tlwhs[0],
391
+ 'online_scores': online_scores[0],
392
+ 'online_ids': online_ids[0],
393
+ }
394
+ seq_name = det_results['seq_name']
395
+ frame_id = det_results['frame_id']
396
+ tracking_outs['feat_data'] = {}
397
+ for _tlbr, _id, _feat in zip(online_tlbrs[0], online_ids[0],
398
+ online_feats[0]):
399
+ feat_data = {}
400
+ feat_data['bbox'] = _tlbr
401
+ feat_data['frame'] = f"{frame_id:06d}"
402
+ feat_data['id'] = _id
403
+ _imgname = f'{seq_name}_{_id}_{frame_id}.jpg'
404
+ feat_data['imgname'] = _imgname
405
+ feat_data['feat'] = _feat
406
+ tracking_outs['feat_data'].update({_imgname: feat_data})
407
+ return tracking_outs
408
+
409
+ else:
410
+ tracking_outs = {
411
+ 'online_tlwhs': online_tlwhs,
412
+ 'online_scores': online_scores,
413
+ 'online_ids': online_ids,
414
+ }
415
+ return tracking_outs
416
+
417
+ def predict_image(self,
418
+ image_list,
419
+ run_benchmark=False,
420
+ repeats=1,
421
+ visual=True,
422
+ seq_name=None,
423
+ reuse_det_result=False):
424
+ num_classes = self.num_classes
425
+ image_list.sort()
426
+ ids2names = self.pred_config.labels
427
+ if self.do_mtmct:
428
+ mot_features_dict = {} # cid_tid_fid feats
429
+ else:
430
+ mot_results = []
431
+ for frame_id, img_file in enumerate(image_list):
432
+ if self.do_mtmct:
433
+ if frame_id % 10 == 0:
434
+ print('Tracking frame: %d' % (frame_id))
435
+ batch_image_list = [img_file] # bs=1 in MOT model
436
+ frame, _ = decode_image(img_file, {})
437
+ if run_benchmark:
438
+ # preprocess
439
+ inputs = self.preprocess(batch_image_list) # warmup
440
+ self.det_times.preprocess_time_s.start()
441
+ inputs = self.preprocess(batch_image_list)
442
+ self.det_times.preprocess_time_s.end()
443
+
444
+ # model prediction
445
+ result_warmup = self.predict(repeats=repeats) # warmup
446
+ self.det_times.inference_time_s.start()
447
+ result = self.predict(repeats=repeats)
448
+ self.det_times.inference_time_s.end(repeats=repeats)
449
+
450
+ # postprocess
451
+ result_warmup = self.postprocess(inputs, result) # warmup
452
+ self.det_times.postprocess_time_s.start()
453
+ det_result = self.postprocess(inputs, result)
454
+ self.det_times.postprocess_time_s.end()
455
+
456
+ # tracking
457
+ if self.use_reid:
458
+ det_result['frame_id'] = frame_id
459
+ det_result['seq_name'] = seq_name
460
+ det_result['ori_image'] = frame
461
+ det_result = self.reidprocess(det_result)
462
+ result_warmup = self.tracking(det_result)
463
+ self.det_times.tracking_time_s.start()
464
+ if self.use_reid:
465
+ det_result = self.reidprocess(det_result)
466
+ tracking_outs = self.tracking(det_result)
467
+ self.det_times.tracking_time_s.end()
468
+ self.det_times.img_num += 1
469
+
470
+ cm, gm, gu = get_current_memory_mb()
471
+ self.cpu_mem += cm
472
+ self.gpu_mem += gm
473
+ self.gpu_util += gu
474
+
475
+ else:
476
+ self.det_times.preprocess_time_s.start()
477
+ if not reuse_det_result:
478
+ inputs = self.preprocess(batch_image_list)
479
+ self.det_times.preprocess_time_s.end()
480
+
481
+ self.det_times.inference_time_s.start()
482
+ if not reuse_det_result:
483
+ result = self.predict()
484
+ self.det_times.inference_time_s.end()
485
+
486
+ self.det_times.postprocess_time_s.start()
487
+ if not reuse_det_result:
488
+ det_result = self.postprocess(inputs, result)
489
+ self.previous_det_result = det_result
490
+ else:
491
+ assert self.previous_det_result is not None
492
+ det_result = self.previous_det_result
493
+ self.det_times.postprocess_time_s.end()
494
+
495
+ # tracking process
496
+ self.det_times.tracking_time_s.start()
497
+ if self.use_reid:
498
+ det_result['frame_id'] = frame_id
499
+ det_result['seq_name'] = seq_name
500
+ det_result['ori_image'] = frame
501
+ det_result = self.reidprocess(det_result)
502
+ tracking_outs = self.tracking(det_result)
503
+ self.det_times.tracking_time_s.end()
504
+ self.det_times.img_num += 1
505
+
506
+ online_tlwhs = tracking_outs['online_tlwhs']
507
+ online_scores = tracking_outs['online_scores']
508
+ online_ids = tracking_outs['online_ids']
509
+
510
+ if self.do_mtmct:
511
+ feat_data_dict = tracking_outs['feat_data']
512
+ mot_features_dict = dict(mot_features_dict, **feat_data_dict)
513
+ else:
514
+ mot_results.append([online_tlwhs, online_scores, online_ids])
515
+
516
+ if visual:
517
+ if len(image_list) > 1 and frame_id % 10 == 0:
518
+ print('Tracking frame {}'.format(frame_id))
519
+ frame, _ = decode_image(img_file, {})
520
+ if isinstance(online_tlwhs, defaultdict):
521
+ im = plot_tracking_dict(
522
+ frame,
523
+ num_classes,
524
+ online_tlwhs,
525
+ online_ids,
526
+ online_scores,
527
+ frame_id=frame_id,
528
+ ids2names=ids2names)
529
+ else:
530
+ im = plot_tracking(
531
+ frame,
532
+ online_tlwhs,
533
+ online_ids,
534
+ online_scores,
535
+ frame_id=frame_id,
536
+ ids2names=ids2names)
537
+ save_dir = os.path.join(self.output_dir, seq_name)
538
+ if not os.path.exists(save_dir):
539
+ os.makedirs(save_dir)
540
+ cv2.imwrite(
541
+ os.path.join(save_dir, '{:05d}.jpg'.format(frame_id)), im)
542
+
543
+ if self.do_mtmct:
544
+ return mot_features_dict
545
+ else:
546
+ return mot_results
547
+
548
+ def predict_video(self, video_file, camera_id):
549
+ video_out_name = 'output.mp4'
550
+ if camera_id != -1:
551
+ capture = cv2.VideoCapture(camera_id)
552
+ else:
553
+ capture = cv2.VideoCapture(video_file)
554
+ video_out_name = os.path.split(video_file)[-1]
555
+ # Get Video info : resolution, fps, frame count
556
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
557
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
558
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
559
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
560
+ print("fps: %d, frame_count: %d" % (fps, frame_count))
561
+
562
+ if not os.path.exists(self.output_dir):
563
+ os.makedirs(self.output_dir)
564
+ out_path = os.path.join(self.output_dir, video_out_name)
565
+ video_format = 'mp4v'
566
+ fourcc = cv2.VideoWriter_fourcc(*video_format)
567
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
568
+
569
+ frame_id = 0
570
+ timer = MOTTimer()
571
+ results = defaultdict(list)
572
+ num_classes = self.num_classes
573
+ data_type = 'mcmot' if num_classes > 1 else 'mot'
574
+ ids2names = self.pred_config.labels
575
+
576
+ center_traj = None
577
+ entrance = None
578
+ records = None
579
+ if self.draw_center_traj:
580
+ center_traj = [{} for i in range(num_classes)]
581
+ if num_classes == 1:
582
+ id_set = set()
583
+ interval_id_set = set()
584
+ in_id_list = list()
585
+ out_id_list = list()
586
+ prev_center = dict()
587
+ records = list()
588
+ if self.do_entrance_counting or self.do_break_in_counting:
589
+ if self.region_type == 'horizontal':
590
+ entrance = [0, height / 2., width, height / 2.]
591
+ elif self.region_type == 'vertical':
592
+ entrance = [width / 2, 0., width / 2, height]
593
+ elif self.region_type == 'custom':
594
+ entrance = []
595
+ assert len(
596
+ self.region_polygon
597
+ ) % 2 == 0, "region_polygon should be pairs of coords points when do break_in counting."
598
+ for i in range(0, len(self.region_polygon), 2):
599
+ entrance.append([
600
+ self.region_polygon[i], self.region_polygon[i + 1]
601
+ ])
602
+ entrance.append([width, height])
603
+ else:
604
+ raise ValueError("region_type:{} is not supported.".format(
605
+ self.region_type))
606
+
607
+ video_fps = fps
608
+
609
+ while (1):
610
+ ret, frame = capture.read()
611
+ if not ret:
612
+ break
613
+ if frame_id % 10 == 0:
614
+ print('Tracking frame: %d' % (frame_id))
615
+
616
+ timer.tic()
617
+ mot_skip_frame_num = self.skip_frame_num
618
+ reuse_det_result = False
619
+ if mot_skip_frame_num > 1 and frame_id > 0 and frame_id % mot_skip_frame_num > 0:
620
+ reuse_det_result = True
621
+ seq_name = video_out_name.split('.')[0]
622
+ mot_results = self.predict_image(
623
+ [frame],
624
+ visual=False,
625
+ seq_name=seq_name,
626
+ reuse_det_result=reuse_det_result)
627
+ timer.toc()
628
+
629
+ # bs=1 in MOT model
630
+ online_tlwhs, online_scores, online_ids = mot_results[0]
631
+
632
+ # flow statistic for one class, and only for bytetracker
633
+ if num_classes == 1 and not self.use_deepsort_tracker and not self.use_ocsort_tracker:
634
+ result = (frame_id + 1, online_tlwhs[0], online_scores[0],
635
+ online_ids[0])
636
+ statistic = flow_statistic(
637
+ result,
638
+ self.secs_interval,
639
+ self.do_entrance_counting,
640
+ self.do_break_in_counting,
641
+ self.region_type,
642
+ video_fps,
643
+ entrance,
644
+ id_set,
645
+ interval_id_set,
646
+ in_id_list,
647
+ out_id_list,
648
+ prev_center,
649
+ records,
650
+ data_type,
651
+ ids2names=self.pred_config.labels)
652
+ records = statistic['records']
653
+
654
+ fps = 1. / timer.duration
655
+ if self.use_deepsort_tracker or self.use_ocsort_tracker:
656
+ # use DeepSORTTracker or OCSORTTracker, only support singe class
657
+ results[0].append(
658
+ (frame_id + 1, online_tlwhs, online_scores, online_ids))
659
+ im = plot_tracking(
660
+ frame,
661
+ online_tlwhs,
662
+ online_ids,
663
+ online_scores,
664
+ frame_id=frame_id,
665
+ fps=fps,
666
+ ids2names=ids2names,
667
+ do_entrance_counting=self.do_entrance_counting,
668
+ entrance=entrance)
669
+ else:
670
+ # use ByteTracker, support multiple class
671
+ for cls_id in range(num_classes):
672
+ results[cls_id].append(
673
+ (frame_id + 1, online_tlwhs[cls_id],
674
+ online_scores[cls_id], online_ids[cls_id]))
675
+ im = plot_tracking_dict(
676
+ frame,
677
+ num_classes,
678
+ online_tlwhs,
679
+ online_ids,
680
+ online_scores,
681
+ frame_id=frame_id,
682
+ fps=fps,
683
+ ids2names=ids2names,
684
+ do_entrance_counting=self.do_entrance_counting,
685
+ entrance=entrance,
686
+ records=records,
687
+ center_traj=center_traj)
688
+
689
+ writer.write(im)
690
+ if camera_id != -1:
691
+ cv2.imshow('Mask Detection', im)
692
+ if cv2.waitKey(1) & 0xFF == ord('q'):
693
+ break
694
+ frame_id += 1
695
+
696
+ if self.save_mot_txts:
697
+ result_filename = os.path.join(
698
+ self.output_dir, video_out_name.split('.')[-2] + '.txt')
699
+ write_mot_results(result_filename, results)
700
+
701
+ result_filename = os.path.join(
702
+ self.output_dir,
703
+ video_out_name.split('.')[-2] + '_flow_statistic.txt')
704
+ f = open(result_filename, 'w')
705
+ for line in records:
706
+ f.write(line)
707
+ print('Flow statistic save in {}'.format(result_filename))
708
+ f.close()
709
+
710
+ writer.release()
711
+
712
+ def predict_mtmct(self, mtmct_dir, mtmct_cfg):
713
+ cameras_bias = mtmct_cfg['cameras_bias']
714
+ cid_bias = parse_bias(cameras_bias)
715
+ scene_cluster = list(cid_bias.keys())
716
+ # 1.zone releated parameters
717
+ use_zone = mtmct_cfg.get('use_zone', False)
718
+ zone_path = mtmct_cfg.get('zone_path', None)
719
+
720
+ # 2.tricks parameters, can be used for other mtmct dataset
721
+ use_ff = mtmct_cfg.get('use_ff', False)
722
+ use_rerank = mtmct_cfg.get('use_rerank', False)
723
+
724
+ # 3.camera releated parameters
725
+ use_camera = mtmct_cfg.get('use_camera', False)
726
+ use_st_filter = mtmct_cfg.get('use_st_filter', False)
727
+
728
+ # 4.zone releated parameters
729
+ use_roi = mtmct_cfg.get('use_roi', False)
730
+ roi_dir = mtmct_cfg.get('roi_dir', False)
731
+
732
+ mot_list_breaks = []
733
+ cid_tid_dict = dict()
734
+
735
+ output_dir = self.output_dir
736
+ if not os.path.exists(output_dir):
737
+ os.makedirs(output_dir)
738
+
739
+ seqs = os.listdir(mtmct_dir)
740
+ for seq in sorted(seqs):
741
+ fpath = os.path.join(mtmct_dir, seq)
742
+ if os.path.isfile(fpath) and _is_valid_video(fpath):
743
+ seq = seq.split('.')[-2]
744
+ print('ffmpeg processing of video {}'.format(fpath))
745
+ frames_path = video2frames(
746
+ video_path=fpath, outpath=mtmct_dir, frame_rate=25)
747
+ fpath = os.path.join(mtmct_dir, seq)
748
+
749
+ if os.path.isdir(fpath) == False:
750
+ print('{} is not a image folder.'.format(fpath))
751
+ continue
752
+ if os.path.exists(os.path.join(fpath, 'img1')):
753
+ fpath = os.path.join(fpath, 'img1')
754
+ assert os.path.isdir(fpath), '{} should be a directory'.format(
755
+ fpath)
756
+ image_list = glob.glob(os.path.join(fpath, '*.jpg'))
757
+ image_list.sort()
758
+ assert len(image_list) > 0, '{} has no images.'.format(fpath)
759
+ print('start tracking seq: {}'.format(seq))
760
+
761
+ mot_features_dict = self.predict_image(
762
+ image_list, visual=False, seq_name=seq)
763
+
764
+ cid = int(re.sub('[a-z,A-Z]', "", seq))
765
+ tid_data, mot_list_break = trajectory_fusion(
766
+ mot_features_dict,
767
+ cid,
768
+ cid_bias,
769
+ use_zone=use_zone,
770
+ zone_path=zone_path)
771
+ mot_list_breaks.append(mot_list_break)
772
+ # single seq process
773
+ for line in tid_data:
774
+ tracklet = tid_data[line]
775
+ tid = tracklet['tid']
776
+ if (cid, tid) not in cid_tid_dict:
777
+ cid_tid_dict[(cid, tid)] = tracklet
778
+
779
+ map_tid = sub_cluster(
780
+ cid_tid_dict,
781
+ scene_cluster,
782
+ use_ff=use_ff,
783
+ use_rerank=use_rerank,
784
+ use_camera=use_camera,
785
+ use_st_filter=use_st_filter)
786
+
787
+ pred_mtmct_file = os.path.join(output_dir, 'mtmct_result.txt')
788
+ if use_camera:
789
+ gen_res(pred_mtmct_file, scene_cluster, map_tid, mot_list_breaks)
790
+ else:
791
+ gen_res(
792
+ pred_mtmct_file,
793
+ scene_cluster,
794
+ map_tid,
795
+ mot_list_breaks,
796
+ use_roi=use_roi,
797
+ roi_dir=roi_dir)
798
+
799
+ camera_results, cid_tid_fid_res = get_mtmct_matching_results(
800
+ pred_mtmct_file)
801
+
802
+ crops_dir = os.path.join(output_dir, 'mtmct_crops')
803
+ save_mtmct_crops(
804
+ cid_tid_fid_res, images_dir=mtmct_dir, crops_dir=crops_dir)
805
+
806
+ save_dir = os.path.join(output_dir, 'mtmct_vis')
807
+ save_mtmct_vis_results(
808
+ camera_results,
809
+ images_dir=mtmct_dir,
810
+ save_dir=save_dir,
811
+ save_videos=FLAGS.save_images)
812
+
813
+
814
+ def main():
815
+ deploy_file = os.path.join(FLAGS.model_dir, 'infer_cfg.yml')
816
+ with open(deploy_file) as f:
817
+ yml_conf = yaml.safe_load(f)
818
+ arch = yml_conf['arch']
819
+ detector = SDE_Detector(
820
+ FLAGS.model_dir,
821
+ tracker_config=FLAGS.tracker_config,
822
+ device=FLAGS.device,
823
+ run_mode=FLAGS.run_mode,
824
+ batch_size=1,
825
+ trt_min_shape=FLAGS.trt_min_shape,
826
+ trt_max_shape=FLAGS.trt_max_shape,
827
+ trt_opt_shape=FLAGS.trt_opt_shape,
828
+ trt_calib_mode=FLAGS.trt_calib_mode,
829
+ cpu_threads=FLAGS.cpu_threads,
830
+ enable_mkldnn=FLAGS.enable_mkldnn,
831
+ output_dir=FLAGS.output_dir,
832
+ threshold=FLAGS.threshold,
833
+ save_images=FLAGS.save_images,
834
+ save_mot_txts=FLAGS.save_mot_txts,
835
+ draw_center_traj=FLAGS.draw_center_traj,
836
+ secs_interval=FLAGS.secs_interval,
837
+ skip_frame_num=FLAGS.skip_frame_num,
838
+ do_entrance_counting=FLAGS.do_entrance_counting,
839
+ do_break_in_counting=FLAGS.do_break_in_counting,
840
+ region_type=FLAGS.region_type,
841
+ region_polygon=FLAGS.region_polygon,
842
+ reid_model_dir=FLAGS.reid_model_dir,
843
+ mtmct_dir=FLAGS.mtmct_dir, )
844
+
845
+ # predict from video file or camera video stream
846
+ if FLAGS.video_file is not None or FLAGS.camera_id != -1:
847
+ detector.predict_video(FLAGS.video_file, FLAGS.camera_id)
848
+ elif FLAGS.mtmct_dir is not None:
849
+ with open(FLAGS.mtmct_cfg) as f:
850
+ mtmct_cfg = yaml.safe_load(f)
851
+ detector.predict_mtmct(FLAGS.mtmct_dir, mtmct_cfg)
852
+ else:
853
+ # predict from image
854
+ if FLAGS.image_dir is None and FLAGS.image_file is not None:
855
+ assert FLAGS.batch_size == 1, "--batch_size should be 1 in MOT models."
856
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
857
+ seq_name = FLAGS.image_dir.split('/')[-1]
858
+ detector.predict_image(
859
+ img_list, FLAGS.run_benchmark, repeats=10, seq_name=seq_name)
860
+
861
+ if not FLAGS.run_benchmark:
862
+ detector.det_times.info(average=True)
863
+ else:
864
+ mode = FLAGS.run_mode
865
+ model_dir = FLAGS.model_dir
866
+ model_info = {
867
+ 'model_name': model_dir.strip('/').split('/')[-1],
868
+ 'precision': mode.split('_')[-1]
869
+ }
870
+ bench_log(detector, img_list, model_info, name='MOT')
871
+
872
+
873
+ if __name__ == '__main__':
874
+ paddle.enable_static()
875
+ parser = argsparser()
876
+ FLAGS = parser.parse_args()
877
+ print_arguments(FLAGS)
878
+ FLAGS.device = FLAGS.device.upper()
879
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
880
+ ], "device should be CPU, GPU or XPU"
881
+
882
+ main()
pptracking/python/mot_utils.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import time
16
+ import os
17
+ import sys
18
+ import ast
19
+ import argparse
20
+
21
+
22
+ def argsparser():
23
+ parser = argparse.ArgumentParser(description=__doc__)
24
+ parser.add_argument(
25
+ "--model_dir",
26
+ type=str,
27
+ default=None,
28
+ help=("Directory include:'model.pdiparams', 'model.pdmodel', "
29
+ "'infer_cfg.yml', created by tools/export_model.py."),
30
+ required=True)
31
+ parser.add_argument(
32
+ "--image_file", type=str, default=None, help="Path of image file.")
33
+ parser.add_argument(
34
+ "--image_dir",
35
+ type=str,
36
+ default=None,
37
+ help="Dir of image file, `image_file` has a higher priority.")
38
+ parser.add_argument(
39
+ "--batch_size", type=int, default=1, help="batch_size for inference.")
40
+ parser.add_argument(
41
+ "--video_file",
42
+ type=str,
43
+ default=None,
44
+ help="Path of video file, `video_file` or `camera_id` has a highest priority."
45
+ )
46
+ parser.add_argument(
47
+ "--camera_id",
48
+ type=int,
49
+ default=-1,
50
+ help="device id of camera to predict.")
51
+ parser.add_argument(
52
+ "--threshold", type=float, default=0.5, help="Threshold of score.")
53
+ parser.add_argument(
54
+ "--output_dir",
55
+ type=str,
56
+ default="output",
57
+ help="Directory of output visualization files.")
58
+ parser.add_argument(
59
+ "--run_mode",
60
+ type=str,
61
+ default='paddle',
62
+ help="mode of running(paddle/trt_fp32/trt_fp16/trt_int8)")
63
+ parser.add_argument(
64
+ "--device",
65
+ type=str,
66
+ default='cpu',
67
+ help="Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU."
68
+ )
69
+ parser.add_argument(
70
+ "--use_gpu",
71
+ type=ast.literal_eval,
72
+ default=False,
73
+ help="Deprecated, please use `--device`.")
74
+ parser.add_argument(
75
+ "--run_benchmark",
76
+ type=ast.literal_eval,
77
+ default=False,
78
+ help="Whether to predict a image_file repeatedly for benchmark")
79
+ parser.add_argument(
80
+ "--enable_mkldnn",
81
+ type=ast.literal_eval,
82
+ default=False,
83
+ help="Whether use mkldnn with CPU.")
84
+ parser.add_argument(
85
+ "--cpu_threads", type=int, default=1, help="Num of threads with CPU.")
86
+ parser.add_argument(
87
+ "--trt_min_shape", type=int, default=1, help="min_shape for TensorRT.")
88
+ parser.add_argument(
89
+ "--trt_max_shape",
90
+ type=int,
91
+ default=1280,
92
+ help="max_shape for TensorRT.")
93
+ parser.add_argument(
94
+ "--trt_opt_shape",
95
+ type=int,
96
+ default=640,
97
+ help="opt_shape for TensorRT.")
98
+ parser.add_argument(
99
+ "--trt_calib_mode",
100
+ type=bool,
101
+ default=False,
102
+ help="If the model is produced by TRT offline quantitative "
103
+ "calibration, trt_calib_mode need to set True.")
104
+ parser.add_argument(
105
+ '--save_images',
106
+ action='store_true',
107
+ help='Save visualization image results.')
108
+ parser.add_argument(
109
+ '--save_mot_txts',
110
+ action='store_true',
111
+ help='Save tracking results (txt).')
112
+ parser.add_argument(
113
+ '--save_mot_txt_per_img',
114
+ action='store_true',
115
+ help='Save tracking results (txt) for each image.')
116
+ parser.add_argument(
117
+ '--scaled',
118
+ type=bool,
119
+ default=False,
120
+ help="Whether coords after detector outputs are scaled, False in JDE YOLOv3 "
121
+ "True in general detector.")
122
+ parser.add_argument(
123
+ "--tracker_config", type=str, default=None, help=("tracker donfig"))
124
+ parser.add_argument(
125
+ "--reid_model_dir",
126
+ type=str,
127
+ default=None,
128
+ help=("Directory include:'model.pdiparams', 'model.pdmodel', "
129
+ "'infer_cfg.yml', created by tools/export_model.py."))
130
+ parser.add_argument(
131
+ "--reid_batch_size",
132
+ type=int,
133
+ default=50,
134
+ help="max batch_size for reid model inference.")
135
+ parser.add_argument(
136
+ '--use_dark',
137
+ type=ast.literal_eval,
138
+ default=True,
139
+ help='whether to use darkpose to get better keypoint position predict ')
140
+ parser.add_argument(
141
+ '--skip_frame_num',
142
+ type=int,
143
+ default=-1,
144
+ help='Skip frames to speed up the process of getting mot results.')
145
+ parser.add_argument(
146
+ "--do_entrance_counting",
147
+ action='store_true',
148
+ help="Whether counting the numbers of identifiers entering "
149
+ "or getting out from the entrance. Note that only support single-class MOT."
150
+ )
151
+ parser.add_argument(
152
+ "--do_break_in_counting",
153
+ action='store_true',
154
+ help="Whether counting the numbers of identifiers break in "
155
+ "the area. Note that only support single-class MOT and "
156
+ "the video should be taken by a static camera.")
157
+ parser.add_argument(
158
+ "--region_type",
159
+ type=str,
160
+ default='horizontal',
161
+ help="Area type for entrance counting or break in counting, 'horizontal' and "
162
+ "'vertical' used when do entrance counting. 'custom' used when do break in counting. "
163
+ "Note that only support single-class MOT, and the video should be taken by a static camera."
164
+ )
165
+ parser.add_argument(
166
+ '--region_polygon',
167
+ nargs='+',
168
+ type=int,
169
+ default=[],
170
+ help="Clockwise point coords (x0,y0,x1,y1...) of polygon of area when "
171
+ "do_break_in_counting. Note that only support single-class MOT and "
172
+ "the video should be taken by a static camera.")
173
+ parser.add_argument(
174
+ "--secs_interval",
175
+ type=int,
176
+ default=2,
177
+ help="The seconds interval to count after tracking")
178
+ parser.add_argument(
179
+ "--draw_center_traj",
180
+ action='store_true',
181
+ help="Whether drawing the trajectory of center")
182
+ parser.add_argument(
183
+ "--mtmct_dir",
184
+ type=str,
185
+ default=None,
186
+ help="The MTMCT scene video folder.")
187
+ parser.add_argument(
188
+ "--mtmct_cfg", type=str, default=None, help="The MTMCT config.")
189
+ return parser
190
+
191
+
192
+ class Times(object):
193
+ def __init__(self):
194
+ self.time = 0.
195
+ # start time
196
+ self.st = 0.
197
+ # end time
198
+ self.et = 0.
199
+
200
+ def start(self):
201
+ self.st = time.time()
202
+
203
+ def end(self, repeats=1, accumulative=True):
204
+ self.et = time.time()
205
+ if accumulative:
206
+ self.time += (self.et - self.st) / repeats
207
+ else:
208
+ self.time = (self.et - self.st) / repeats
209
+
210
+ def reset(self):
211
+ self.time = 0.
212
+ self.st = 0.
213
+ self.et = 0.
214
+
215
+ def value(self):
216
+ return round(self.time, 4)
217
+
218
+
219
+ class Timer(Times):
220
+ def __init__(self, with_tracker=False):
221
+ super(Timer, self).__init__()
222
+ self.with_tracker = with_tracker
223
+ self.preprocess_time_s = Times()
224
+ self.inference_time_s = Times()
225
+ self.postprocess_time_s = Times()
226
+ self.tracking_time_s = Times()
227
+ self.img_num = 0
228
+
229
+ def info(self, average=False):
230
+ pre_time = self.preprocess_time_s.value()
231
+ infer_time = self.inference_time_s.value()
232
+ post_time = self.postprocess_time_s.value()
233
+ track_time = self.tracking_time_s.value()
234
+
235
+ total_time = pre_time + infer_time + post_time
236
+ if self.with_tracker:
237
+ total_time = total_time + track_time
238
+ total_time = round(total_time, 4)
239
+ print("------------------ Inference Time Info ----------------------")
240
+ print("total_time(ms): {}, img_num: {}".format(total_time * 1000,
241
+ self.img_num))
242
+ preprocess_time = round(pre_time / max(1, self.img_num),
243
+ 4) if average else pre_time
244
+ postprocess_time = round(post_time / max(1, self.img_num),
245
+ 4) if average else post_time
246
+ inference_time = round(infer_time / max(1, self.img_num),
247
+ 4) if average else infer_time
248
+ tracking_time = round(track_time / max(1, self.img_num),
249
+ 4) if average else track_time
250
+
251
+ average_latency = total_time / max(1, self.img_num)
252
+ qps = 0
253
+ if total_time > 0:
254
+ qps = 1 / average_latency
255
+ print("average latency time(ms): {:.2f}, QPS: {:2f}".format(
256
+ average_latency * 1000, qps))
257
+ if self.with_tracker:
258
+ print(
259
+ "preprocess_time(ms): {:.2f}, inference_time(ms): {:.2f}, postprocess_time(ms): {:.2f}, tracking_time(ms): {:.2f}".
260
+ format(preprocess_time * 1000, inference_time * 1000,
261
+ postprocess_time * 1000, tracking_time * 1000))
262
+ else:
263
+ print(
264
+ "preprocess_time(ms): {:.2f}, inference_time(ms): {:.2f}, postprocess_time(ms): {:.2f}".
265
+ format(preprocess_time * 1000, inference_time * 1000,
266
+ postprocess_time * 1000))
267
+
268
+ def report(self, average=False):
269
+ dic = {}
270
+ pre_time = self.preprocess_time_s.value()
271
+ infer_time = self.inference_time_s.value()
272
+ post_time = self.postprocess_time_s.value()
273
+ track_time = self.tracking_time_s.value()
274
+
275
+ dic['preprocess_time_s'] = round(pre_time / max(1, self.img_num),
276
+ 4) if average else pre_time
277
+ dic['inference_time_s'] = round(infer_time / max(1, self.img_num),
278
+ 4) if average else infer_time
279
+ dic['postprocess_time_s'] = round(post_time / max(1, self.img_num),
280
+ 4) if average else post_time
281
+ dic['img_num'] = self.img_num
282
+ total_time = pre_time + infer_time + post_time
283
+ if self.with_tracker:
284
+ dic['tracking_time_s'] = round(track_time / max(1, self.img_num),
285
+ 4) if average else track_time
286
+ total_time = total_time + track_time
287
+ dic['total_time_s'] = round(total_time, 4)
288
+ return dic
289
+
290
+
291
+ def get_current_memory_mb():
292
+ """
293
+ It is used to Obtain the memory usage of the CPU and GPU during the running of the program.
294
+ And this function Current program is time-consuming.
295
+ """
296
+ import pynvml
297
+ import psutil
298
+ import GPUtil
299
+ gpu_id = int(os.environ.get('CUDA_VISIBLE_DEVICES', 0))
300
+
301
+ pid = os.getpid()
302
+ p = psutil.Process(pid)
303
+ info = p.memory_full_info()
304
+ cpu_mem = info.uss / 1024. / 1024.
305
+ gpu_mem = 0
306
+ gpu_percent = 0
307
+ gpus = GPUtil.getGPUs()
308
+ if gpu_id is not None and len(gpus) > 0:
309
+ gpu_percent = gpus[gpu_id].load
310
+ pynvml.nvmlInit()
311
+ handle = pynvml.nvmlDeviceGetHandleByIndex(0)
312
+ meminfo = pynvml.nvmlDeviceGetMemoryInfo(handle)
313
+ gpu_mem = meminfo.used / 1024. / 1024.
314
+ return round(cpu_mem, 4), round(gpu_mem, 4), round(gpu_percent, 4)
315
+
316
+
317
+ def video2frames(video_path, outpath, frame_rate=25, **kargs):
318
+ def _dict2str(kargs):
319
+ cmd_str = ''
320
+ for k, v in kargs.items():
321
+ cmd_str += (' ' + str(k) + ' ' + str(v))
322
+ return cmd_str
323
+
324
+ ffmpeg = ['ffmpeg ', ' -y -loglevel ', ' error ']
325
+ vid_name = os.path.basename(video_path).split('.')[0]
326
+ out_full_path = os.path.join(outpath, vid_name)
327
+
328
+ if not os.path.exists(out_full_path):
329
+ os.makedirs(out_full_path)
330
+
331
+ # video file name
332
+ outformat = os.path.join(out_full_path, '%05d.jpg')
333
+
334
+ cmd = ffmpeg
335
+ cmd = ffmpeg + [
336
+ ' -i ', video_path, ' -r ', str(frame_rate), ' -f image2 ', outformat
337
+ ]
338
+ cmd = ''.join(cmd) + _dict2str(kargs)
339
+
340
+ if os.system(cmd) != 0:
341
+ raise RuntimeError('ffmpeg process video: {} error'.format(video_path))
342
+ sys.exit(-1)
343
+
344
+ sys.stdout.flush()
345
+ return out_full_path
346
+
347
+
348
+ def _is_valid_video(f, extensions=('.mp4', '.avi', '.mov', '.rmvb', '.flv')):
349
+ return f.lower().endswith(extensions)
pptracking/python/mtmct_cfg.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config for MTMCT
2
+ MTMCT: True
3
+ cameras_bias: # default for scene S01. For S06, should modify as 'c041: 0 c042: 0'
4
+ c003: 0
5
+ c004: 0
6
+ # 1.zone releated parameters
7
+ use_zone: False
8
+ zone_path: dataset/mot/aic21mtmct_vehicle/S06/zone
9
+ # 2.tricks parameters, can be used for other mtmct dataset
10
+ use_ff: False
11
+ use_rerank: False
12
+ # 3.camera releated parameters
13
+ use_camera: False
14
+ use_st_filter: False
15
+ # 4.zone releated parameters
16
+ use_roi: False
17
+ roi_dir: dataset/mot/aic21mtmct_vehicle/S06
pptracking/python/picodet_postprocess.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import numpy as np
16
+ from scipy.special import softmax
17
+
18
+
19
+ def hard_nms(box_scores, iou_threshold, top_k=-1, candidate_size=200):
20
+ """
21
+ Args:
22
+ box_scores (N, 5): boxes in corner-form and probabilities.
23
+ iou_threshold: intersection over union threshold.
24
+ top_k: keep top_k results. If k <= 0, keep all the results.
25
+ candidate_size: only consider the candidates with the highest scores.
26
+ Returns:
27
+ picked: a list of indexes of the kept boxes
28
+ """
29
+ scores = box_scores[:, -1]
30
+ boxes = box_scores[:, :-1]
31
+ picked = []
32
+ indexes = np.argsort(scores)
33
+ indexes = indexes[-candidate_size:]
34
+ while len(indexes) > 0:
35
+ current = indexes[-1]
36
+ picked.append(current)
37
+ if 0 < top_k == len(picked) or len(indexes) == 1:
38
+ break
39
+ current_box = boxes[current, :]
40
+ indexes = indexes[:-1]
41
+ rest_boxes = boxes[indexes, :]
42
+ iou = iou_of(
43
+ rest_boxes,
44
+ np.expand_dims(
45
+ current_box, axis=0), )
46
+ indexes = indexes[iou <= iou_threshold]
47
+
48
+ return box_scores[picked, :]
49
+
50
+
51
+ def iou_of(boxes0, boxes1, eps=1e-5):
52
+ """Return intersection-over-union (Jaccard index) of boxes.
53
+ Args:
54
+ boxes0 (N, 4): ground truth boxes.
55
+ boxes1 (N or 1, 4): predicted boxes.
56
+ eps: a small number to avoid 0 as denominator.
57
+ Returns:
58
+ iou (N): IoU values.
59
+ """
60
+ overlap_left_top = np.maximum(boxes0[..., :2], boxes1[..., :2])
61
+ overlap_right_bottom = np.minimum(boxes0[..., 2:], boxes1[..., 2:])
62
+
63
+ overlap_area = area_of(overlap_left_top, overlap_right_bottom)
64
+ area0 = area_of(boxes0[..., :2], boxes0[..., 2:])
65
+ area1 = area_of(boxes1[..., :2], boxes1[..., 2:])
66
+ return overlap_area / (area0 + area1 - overlap_area + eps)
67
+
68
+
69
+ def area_of(left_top, right_bottom):
70
+ """Compute the areas of rectangles given two corners.
71
+ Args:
72
+ left_top (N, 2): left top corner.
73
+ right_bottom (N, 2): right bottom corner.
74
+ Returns:
75
+ area (N): return the area.
76
+ """
77
+ hw = np.clip(right_bottom - left_top, 0.0, None)
78
+ return hw[..., 0] * hw[..., 1]
79
+
80
+
81
+ class PicoDetPostProcess(object):
82
+ """
83
+ Args:
84
+ input_shape (int): network input image size
85
+ ori_shape (int): ori image shape of before padding
86
+ scale_factor (float): scale factor of ori image
87
+ enable_mkldnn (bool): whether to open MKLDNN
88
+ """
89
+
90
+ def __init__(self,
91
+ input_shape,
92
+ ori_shape,
93
+ scale_factor,
94
+ strides=[8, 16, 32, 64],
95
+ score_threshold=0.4,
96
+ nms_threshold=0.5,
97
+ nms_top_k=1000,
98
+ keep_top_k=100):
99
+ self.ori_shape = ori_shape
100
+ self.input_shape = input_shape
101
+ self.scale_factor = scale_factor
102
+ self.strides = strides
103
+ self.score_threshold = score_threshold
104
+ self.nms_threshold = nms_threshold
105
+ self.nms_top_k = nms_top_k
106
+ self.keep_top_k = keep_top_k
107
+
108
+ def warp_boxes(self, boxes, ori_shape):
109
+ """Apply transform to boxes
110
+ """
111
+ width, height = ori_shape[1], ori_shape[0]
112
+ n = len(boxes)
113
+ if n:
114
+ # warp points
115
+ xy = np.ones((n * 4, 3))
116
+ xy[:, :2] = boxes[:, [0, 1, 2, 3, 0, 3, 2, 1]].reshape(
117
+ n * 4, 2) # x1y1, x2y2, x1y2, x2y1
118
+ # xy = xy @ M.T # transform
119
+ xy = (xy[:, :2] / xy[:, 2:3]).reshape(n, 8) # rescale
120
+ # create new boxes
121
+ x = xy[:, [0, 2, 4, 6]]
122
+ y = xy[:, [1, 3, 5, 7]]
123
+ xy = np.concatenate(
124
+ (x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T
125
+ # clip boxes
126
+ xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width)
127
+ xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height)
128
+ return xy.astype(np.float32)
129
+ else:
130
+ return boxes
131
+
132
+ def __call__(self, scores, raw_boxes):
133
+ batch_size = raw_boxes[0].shape[0]
134
+ reg_max = int(raw_boxes[0].shape[-1] / 4 - 1)
135
+ out_boxes_num = []
136
+ out_boxes_list = []
137
+ for batch_id in range(batch_size):
138
+ # generate centers
139
+ decode_boxes = []
140
+ select_scores = []
141
+ for stride, box_distribute, score in zip(self.strides, raw_boxes,
142
+ scores):
143
+ box_distribute = box_distribute[batch_id]
144
+ score = score[batch_id]
145
+ # centers
146
+ fm_h = self.input_shape[0] / stride
147
+ fm_w = self.input_shape[1] / stride
148
+ h_range = np.arange(fm_h)
149
+ w_range = np.arange(fm_w)
150
+ ww, hh = np.meshgrid(w_range, h_range)
151
+ ct_row = (hh.flatten() + 0.5) * stride
152
+ ct_col = (ww.flatten() + 0.5) * stride
153
+ center = np.stack((ct_col, ct_row, ct_col, ct_row), axis=1)
154
+
155
+ # box distribution to distance
156
+ reg_range = np.arange(reg_max + 1)
157
+ box_distance = box_distribute.reshape((-1, reg_max + 1))
158
+ box_distance = softmax(box_distance, axis=1)
159
+ box_distance = box_distance * np.expand_dims(reg_range, axis=0)
160
+ box_distance = np.sum(box_distance, axis=1).reshape((-1, 4))
161
+ box_distance = box_distance * stride
162
+
163
+ # top K candidate
164
+ topk_idx = np.argsort(score.max(axis=1))[::-1]
165
+ topk_idx = topk_idx[:self.nms_top_k]
166
+ center = center[topk_idx]
167
+ score = score[topk_idx]
168
+ box_distance = box_distance[topk_idx]
169
+
170
+ # decode box
171
+ decode_box = center + [-1, -1, 1, 1] * box_distance
172
+
173
+ select_scores.append(score)
174
+ decode_boxes.append(decode_box)
175
+
176
+ # nms
177
+ bboxes = np.concatenate(decode_boxes, axis=0)
178
+ confidences = np.concatenate(select_scores, axis=0)
179
+ picked_box_probs = []
180
+ picked_labels = []
181
+ for class_index in range(0, confidences.shape[1]):
182
+ probs = confidences[:, class_index]
183
+ mask = probs > self.score_threshold
184
+ probs = probs[mask]
185
+ if probs.shape[0] == 0:
186
+ continue
187
+ subset_boxes = bboxes[mask, :]
188
+ box_probs = np.concatenate(
189
+ [subset_boxes, probs.reshape(-1, 1)], axis=1)
190
+ box_probs = hard_nms(
191
+ box_probs,
192
+ iou_threshold=self.nms_threshold,
193
+ top_k=self.keep_top_k, )
194
+ picked_box_probs.append(box_probs)
195
+ picked_labels.extend([class_index] * box_probs.shape[0])
196
+
197
+ if len(picked_box_probs) == 0:
198
+ out_boxes_list.append(np.empty((0, 4)))
199
+ out_boxes_num.append(0)
200
+
201
+ else:
202
+ picked_box_probs = np.concatenate(picked_box_probs)
203
+
204
+ # resize output boxes
205
+ picked_box_probs[:, :4] = self.warp_boxes(
206
+ picked_box_probs[:, :4], self.ori_shape[batch_id])
207
+ im_scale = np.concatenate([
208
+ self.scale_factor[batch_id][::-1],
209
+ self.scale_factor[batch_id][::-1]
210
+ ])
211
+ picked_box_probs[:, :4] /= im_scale
212
+ # clas score box
213
+ out_boxes_list.append(
214
+ np.concatenate(
215
+ [
216
+ np.expand_dims(
217
+ np.array(picked_labels),
218
+ axis=-1), np.expand_dims(
219
+ picked_box_probs[:, 4], axis=-1),
220
+ picked_box_probs[:, :4]
221
+ ],
222
+ axis=1))
223
+ out_boxes_num.append(len(picked_labels))
224
+
225
+ out_boxes_list = np.concatenate(out_boxes_list, axis=0)
226
+ out_boxes_num = np.asarray(out_boxes_num).astype(np.int32)
227
+ return out_boxes_list, out_boxes_num
pptracking/python/preprocess.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import cv2
16
+ import numpy as np
17
+
18
+
19
+ def decode_image(im_file, im_info):
20
+ """read rgb image
21
+ Args:
22
+ im_file (str|np.ndarray): input can be image path or np.ndarray
23
+ im_info (dict): info of image
24
+ Returns:
25
+ im (np.ndarray): processed image (np.ndarray)
26
+ im_info (dict): info of processed image
27
+ """
28
+ if isinstance(im_file, str):
29
+ with open(im_file, 'rb') as f:
30
+ im_read = f.read()
31
+ data = np.frombuffer(im_read, dtype='uint8')
32
+ im = cv2.imdecode(data, 1) # BGR mode, but need RGB mode
33
+ im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
34
+ else:
35
+ im = im_file
36
+ im_info['im_shape'] = np.array(im.shape[:2], dtype=np.float32)
37
+ im_info['scale_factor'] = np.array([1., 1.], dtype=np.float32)
38
+ return im, im_info
39
+
40
+
41
+ class Resize(object):
42
+ """resize image by target_size and max_size
43
+ Args:
44
+ target_size (int): the target size of image
45
+ keep_ratio (bool): whether keep_ratio or not, default true
46
+ interp (int): method of resize
47
+ """
48
+
49
+ def __init__(self, target_size, keep_ratio=True, interp=cv2.INTER_LINEAR):
50
+ if isinstance(target_size, int):
51
+ target_size = [target_size, target_size]
52
+ self.target_size = target_size
53
+ self.keep_ratio = keep_ratio
54
+ self.interp = interp
55
+
56
+ def __call__(self, im, im_info):
57
+ """
58
+ Args:
59
+ im (np.ndarray): image (np.ndarray)
60
+ im_info (dict): info of image
61
+ Returns:
62
+ im (np.ndarray): processed image (np.ndarray)
63
+ im_info (dict): info of processed image
64
+ """
65
+ assert len(self.target_size) == 2
66
+ assert self.target_size[0] > 0 and self.target_size[1] > 0
67
+ im_channel = im.shape[2]
68
+ im_scale_y, im_scale_x = self.generate_scale(im)
69
+ im = cv2.resize(
70
+ im,
71
+ None,
72
+ None,
73
+ fx=im_scale_x,
74
+ fy=im_scale_y,
75
+ interpolation=self.interp)
76
+ im_info['im_shape'] = np.array(im.shape[:2]).astype('float32')
77
+ im_info['scale_factor'] = np.array(
78
+ [im_scale_y, im_scale_x]).astype('float32')
79
+ return im, im_info
80
+
81
+ def generate_scale(self, im):
82
+ """
83
+ Args:
84
+ im (np.ndarray): image (np.ndarray)
85
+ Returns:
86
+ im_scale_x: the resize ratio of X
87
+ im_scale_y: the resize ratio of Y
88
+ """
89
+ origin_shape = im.shape[:2]
90
+ im_c = im.shape[2]
91
+ if self.keep_ratio:
92
+ im_size_min = np.min(origin_shape)
93
+ im_size_max = np.max(origin_shape)
94
+ target_size_min = np.min(self.target_size)
95
+ target_size_max = np.max(self.target_size)
96
+ im_scale = float(target_size_min) / float(im_size_min)
97
+ if np.round(im_scale * im_size_max) > target_size_max:
98
+ im_scale = float(target_size_max) / float(im_size_max)
99
+ im_scale_x = im_scale
100
+ im_scale_y = im_scale
101
+ else:
102
+ resize_h, resize_w = self.target_size
103
+ im_scale_y = resize_h / float(origin_shape[0])
104
+ im_scale_x = resize_w / float(origin_shape[1])
105
+ return im_scale_y, im_scale_x
106
+
107
+
108
+ class NormalizeImage(object):
109
+ """normalize image
110
+ Args:
111
+ mean (list): im - mean
112
+ std (list): im / std
113
+ is_scale (bool): whether need im / 255
114
+ is_channel_first (bool): if True: image shape is CHW, else: HWC
115
+ """
116
+
117
+ def __init__(self, mean, std, is_scale=True):
118
+ self.mean = mean
119
+ self.std = std
120
+ self.is_scale = is_scale
121
+
122
+ def __call__(self, im, im_info):
123
+ """
124
+ Args:
125
+ im (np.ndarray): image (np.ndarray)
126
+ im_info (dict): info of image
127
+ Returns:
128
+ im (np.ndarray): processed image (np.ndarray)
129
+ im_info (dict): info of processed image
130
+ """
131
+ im = im.astype(np.float32, copy=False)
132
+ mean = np.array(self.mean)[np.newaxis, np.newaxis, :]
133
+ std = np.array(self.std)[np.newaxis, np.newaxis, :]
134
+
135
+ if self.is_scale:
136
+ im = im / 255.0
137
+ im -= mean
138
+ im /= std
139
+ return im, im_info
140
+
141
+
142
+ class Permute(object):
143
+ """permute image
144
+ Args:
145
+ to_bgr (bool): whether convert RGB to BGR
146
+ channel_first (bool): whether convert HWC to CHW
147
+ """
148
+
149
+ def __init__(self, ):
150
+ super(Permute, self).__init__()
151
+
152
+ def __call__(self, im, im_info):
153
+ """
154
+ Args:
155
+ im (np.ndarray): image (np.ndarray)
156
+ im_info (dict): info of image
157
+ Returns:
158
+ im (np.ndarray): processed image (np.ndarray)
159
+ im_info (dict): info of processed image
160
+ """
161
+ im = im.transpose((2, 0, 1)).copy()
162
+ return im, im_info
163
+
164
+
165
+ class PadStride(object):
166
+ """ padding image for model with FPN, instead PadBatch(pad_to_stride) in original config
167
+ Args:
168
+ stride (bool): model with FPN need image shape % stride == 0
169
+ """
170
+
171
+ def __init__(self, stride=0):
172
+ self.coarsest_stride = stride
173
+
174
+ def __call__(self, im, im_info):
175
+ """
176
+ Args:
177
+ im (np.ndarray): image (np.ndarray)
178
+ im_info (dict): info of image
179
+ Returns:
180
+ im (np.ndarray): processed image (np.ndarray)
181
+ im_info (dict): info of processed image
182
+ """
183
+ coarsest_stride = self.coarsest_stride
184
+ if coarsest_stride <= 0:
185
+ return im, im_info
186
+ im_c, im_h, im_w = im.shape
187
+ pad_h = int(np.ceil(float(im_h) / coarsest_stride) * coarsest_stride)
188
+ pad_w = int(np.ceil(float(im_w) / coarsest_stride) * coarsest_stride)
189
+ padding_im = np.zeros((im_c, pad_h, pad_w), dtype=np.float32)
190
+ padding_im[:, :im_h, :im_w] = im
191
+ return padding_im, im_info
192
+
193
+
194
+ class LetterBoxResize(object):
195
+ def __init__(self, target_size):
196
+ """
197
+ Resize image to target size, convert normalized xywh to pixel xyxy
198
+ format ([x_center, y_center, width, height] -> [x0, y0, x1, y1]).
199
+ Args:
200
+ target_size (int|list): image target size.
201
+ """
202
+ super(LetterBoxResize, self).__init__()
203
+ if isinstance(target_size, int):
204
+ target_size = [target_size, target_size]
205
+ self.target_size = target_size
206
+
207
+ def letterbox(self, img, height, width, color=(127.5, 127.5, 127.5)):
208
+ # letterbox: resize a rectangular image to a padded rectangular
209
+ shape = img.shape[:2] # [height, width]
210
+ ratio_h = float(height) / shape[0]
211
+ ratio_w = float(width) / shape[1]
212
+ ratio = min(ratio_h, ratio_w)
213
+ new_shape = (round(shape[1] * ratio),
214
+ round(shape[0] * ratio)) # [width, height]
215
+ padw = (width - new_shape[0]) / 2
216
+ padh = (height - new_shape[1]) / 2
217
+ top, bottom = round(padh - 0.1), round(padh + 0.1)
218
+ left, right = round(padw - 0.1), round(padw + 0.1)
219
+
220
+ img = cv2.resize(
221
+ img, new_shape, interpolation=cv2.INTER_AREA) # resized, no border
222
+ img = cv2.copyMakeBorder(
223
+ img, top, bottom, left, right, cv2.BORDER_CONSTANT,
224
+ value=color) # padded rectangular
225
+ return img, ratio, padw, padh
226
+
227
+ def __call__(self, im, im_info):
228
+ """
229
+ Args:
230
+ im (np.ndarray): image (np.ndarray)
231
+ im_info (dict): info of image
232
+ Returns:
233
+ im (np.ndarray): processed image (np.ndarray)
234
+ im_info (dict): info of processed image
235
+ """
236
+ assert len(self.target_size) == 2
237
+ assert self.target_size[0] > 0 and self.target_size[1] > 0
238
+ height, width = self.target_size
239
+ h, w = im.shape[:2]
240
+ im, ratio, padw, padh = self.letterbox(im, height=height, width=width)
241
+
242
+ new_shape = [round(h * ratio), round(w * ratio)]
243
+ im_info['im_shape'] = np.array(new_shape, dtype=np.float32)
244
+ im_info['scale_factor'] = np.array([ratio, ratio], dtype=np.float32)
245
+ return im, im_info
246
+
247
+
248
+ class Pad(object):
249
+ def __init__(self, size, fill_value=[114.0, 114.0, 114.0]):
250
+ """
251
+ Pad image to a specified size.
252
+ Args:
253
+ size (list[int]): image target size
254
+ fill_value (list[float]): rgb value of pad area, default (114.0, 114.0, 114.0)
255
+ """
256
+ super(Pad, self).__init__()
257
+ if isinstance(size, int):
258
+ size = [size, size]
259
+ self.size = size
260
+ self.fill_value = fill_value
261
+
262
+ def __call__(self, im, im_info):
263
+ im_h, im_w = im.shape[:2]
264
+ h, w = self.size
265
+ if h == im_h and w == im_w:
266
+ im = im.astype(np.float32)
267
+ return im, im_info
268
+
269
+ canvas = np.ones((h, w, 3), dtype=np.float32)
270
+ canvas *= np.array(self.fill_value, dtype=np.float32)
271
+ canvas[0:im_h, 0:im_w, :] = im.astype(np.float32)
272
+ im = canvas
273
+ return im, im_info
274
+
275
+
276
+ def preprocess(im, preprocess_ops):
277
+ # process image by preprocess_ops
278
+ im_info = {
279
+ 'scale_factor': np.array(
280
+ [1., 1.], dtype=np.float32),
281
+ 'im_shape': None,
282
+ }
283
+ im, im_info = decode_image(im, im_info)
284
+ for operator in preprocess_ops:
285
+ im, im_info = operator(im, im_info)
286
+ return im, im_info
pptracking/python/tracker_config.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config of tracker for MOT SDE Detector, use 'OCSORTTracker' as default, 'JDETracker' here is just BYTETracker.
2
+ # The tracker of MOT JDE Detector (such as FairMOT) is exported together with the model.
3
+ # Here 'min_box_area' and 'vertical_ratio' are set for pedestrian, you can modify for other objects tracking.
4
+
5
+ type: OCSORTTracker # choose one tracker in ['JDETracker', 'OCSORTTracker', 'DeepSORTTracker']
6
+ # When using for MTMCT(Multi-Target Multi-Camera Tracking), you should modify to 'DeepSORTTracker'
7
+
8
+
9
+ # just as BYTETracker, used for FairMOT in PP-Tracking project and for ByteTrack in PP-Humanv1 project
10
+ JDETracker:
11
+ use_byte: True
12
+ det_thresh: 0.3
13
+ conf_thres: 0.6
14
+ low_conf_thres: 0.1
15
+ match_thres: 0.9
16
+ min_box_area: 0
17
+ vertical_ratio: 0 # 1.6 for pedestrian
18
+
19
+
20
+ # used for OC-SORT in PP-Humanv2 project and PP-Vehicle project
21
+ OCSORTTracker:
22
+ det_thresh: 0.4
23
+ max_age: 30
24
+ min_hits: 3
25
+ iou_threshold: 0.3
26
+ delta_t: 3
27
+ inertia: 0.2
28
+ min_box_area: 0
29
+ vertical_ratio: 0
30
+ use_byte: False
31
+
32
+
33
+ # used for DeepSORT and MTMCT in PP-Tracking project
34
+ DeepSORTTracker:
35
+ input_size: [64, 192] # An unique operation to scale the sub-image of the selected detected boxes to a fixed size
36
+ min_box_area: 0
37
+ vertical_ratio: -1
38
+ budget: 100
39
+ max_age: 70
40
+ n_init: 3
41
+ metric_type: cosine
42
+ matching_threshold: 0.2
43
+ max_iou_distance: 0.9
python/README.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python端预测部署
2
+
3
+ 在PaddlePaddle中预测引擎和训练引擎底层有着不同的优化方法, 预测引擎使用了AnalysisPredictor,专门针对推理进行了优化,是基于[C++预测库](https://www.paddlepaddle.org.cn/documentation/docs/zh/advanced_guide/inference_deployment/inference/native_infer.html)的Python接口,该引擎可以对模型进行多项图优化,减少不必要的内存拷贝。如果用户在部署已训练模型的过程中对性能有较高的要求,我们提供了独立于PaddleDetection的预测脚本,方便用户直接集成部署。
4
+
5
+
6
+ Python端预测部署主要包含两个步骤:
7
+ - 导出预测模型
8
+ - 基于Python进行预测
9
+
10
+ ## 1. 导出预测模型
11
+
12
+ PaddleDetection在训练过程包括网络的前向和优化器相关参数,而在部署过程中,我们只需要前向参数,具体参考:[导出模型](../EXPORT_MODEL.md),例如
13
+
14
+ ```bash
15
+ # 导出YOLOv3检测模型
16
+ python tools/export_model.py -c configs/yolov3/yolov3_darknet53_270e_coco.yml --output_dir=./inference_model \
17
+ -o weights=https://paddledet.bj.bcebos.com/models/yolov3_darknet53_270e_coco.pdparams
18
+
19
+ # 导出HigherHRNet(bottom-up)关键点检测模型
20
+ python tools/export_model.py -c configs/keypoint/higherhrnet/higherhrnet_hrnet_w32_512.yml -o weights=https://paddledet.bj.bcebos.com/models/keypoint/higherhrnet_hrnet_w32_512.pdparams
21
+
22
+ # 导出HRNet(top-down)关键点检测模型
23
+ python tools/export_model.py -c configs/keypoint/hrnet/hrnet_w32_384x288.yml -o weights=https://paddledet.bj.bcebos.com/models/keypoint/hrnet_w32_384x288.pdparams
24
+
25
+ # 导出FairMOT多目标跟踪模型
26
+ python tools/export_model.py -c configs/mot/fairmot/fairmot_dla34_30e_1088x608.yml -o weights=https://paddledet.bj.bcebos.com/models/mot/fairmot_dla34_30e_1088x608.pdparams
27
+
28
+ # 导出ByteTrack多目标跟踪模型(相当于只导出检测器)
29
+ python tools/export_model.py -c configs/mot/bytetrack/detector/ppyoloe_crn_l_36e_640x640_mot17half.yml -o weights=https://paddledet.bj.bcebos.com/models/mot/ppyoloe_crn_l_36e_640x640_mot17half.pdparams
30
+ ```
31
+
32
+ 导出后目录下,包括`infer_cfg.yml`, `model.pdiparams`, `model.pdiparams.info`, `model.pdmodel`四个文件。
33
+
34
+
35
+ ## 2. 基于Python的预测
36
+
37
+ ### 2.1 通用检测
38
+ 在终端输入以下命令进行预测:
39
+ ```bash
40
+ python deploy/python/infer.py --model_dir=./output_inference/yolov3_darknet53_270e_coco --image_file=./demo/000000014439.jpg --device=GPU
41
+ ```
42
+
43
+ ### 2.2 关键点检测
44
+ 在终端输入以下命令进行预测:
45
+ ```bash
46
+ # keypoint top-down(HRNet)/bottom-up(HigherHRNet)单独推理,该模式下top-down模型HRNet只支持单人截图预测
47
+ python deploy/python/keypoint_infer.py --model_dir=output_inference/hrnet_w32_384x288/ --image_file=./demo/hrnet_demo.jpg --device=GPU --threshold=0.5
48
+ python deploy/python/keypoint_infer.py --model_dir=output_inference/higherhrnet_hrnet_w32_512/ --image_file=./demo/000000014439_640x640.jpg --device=GPU --threshold=0.5
49
+
50
+ # detector 检测 + keypoint top-down模型联合部署(联合推理只支持top-down关键点模型)
51
+ python deploy/python/det_keypoint_unite_infer.py --det_model_dir=output_inference/yolov3_darknet53_270e_coco/ --keypoint_model_dir=output_inference/hrnet_w32_384x288/ --video_file={your video name}.mp4 --device=GPU
52
+ ```
53
+ **注意:**
54
+ - 关键点检测模型导出和预测具体可参照[keypoint](../../configs/keypoint/README.md),可分别在各个模型的文档中查找具体用法;
55
+ - 此目录下的关键点检测部署为基础前向功能,更多关键点检测功能可使用PP-Human项目,参照[pipeline](../pipeline/README.md);
56
+
57
+
58
+ ### 2.3 多目标跟踪
59
+ 在终端输入以下命令进行预测:
60
+ ```bash
61
+ # FairMOT跟踪
62
+ python deploy/python/mot_jde_infer.py --model_dir=output_inference/fairmot_dla34_30e_1088x608 --video_file={your video name}.mp4 --device=GPU
63
+
64
+ # ByteTrack跟踪
65
+ python deploy/python/mot_sde_infer.py --model_dir=output_inference/ppyoloe_crn_l_36e_640x640_mot17half/ --tracker_config=deploy/python/tracker_config.yml --video_file={your video name}.mp4 --device=GPU --scaled=True
66
+
67
+ # FairMOT多目标跟踪联合HRNet关键点检测(联合推理只支持top-down关键点模型)
68
+ python deploy/python/mot_keypoint_unite_infer.py --mot_model_dir=output_inference/fairmot_dla34_30e_1088x608/ --keypoint_model_dir=output_inference/hrnet_w32_384x288/ --video_file={your video name}.mp4 --device=GPU
69
+ ```
70
+
71
+ **注意:**
72
+ - 多目标跟踪模型导出和预测具体可参照[mot]](../../configs/mot/README.md),可分别在各个模型的文档中查找具体用法;
73
+ - 此目录下的跟踪部署为基础前向功能以及联合关键点部署,更多跟踪功能可使用PP-Human项目,参照[pipeline](../pipeline/README.md),或PP-Tracking项目(绘制轨迹、出入口流量计数),参照[pptracking](../pptracking/README.md);
74
+
75
+
76
+ 参数说明如下:
77
+
78
+ | 参数 | 是否必须| 含义 |
79
+ |-------|-------|---------------------------------------------------------------------------------------------|
80
+ | --model_dir | Yes| 上述导出的模型路径 |
81
+ | --image_file | Option | 需要预测的图片 |
82
+ | --image_dir | Option | 要预测的图片文件夹路径 |
83
+ | --video_file | Option | 需要预测的视频 |
84
+ | --camera_id | Option | 用来预测的摄像头ID,默认为-1(表示不使用摄像头预测,可设置为:0 - (摄像头数目-1) ),预测过程中在可视化界面按`q`退出输出预测结果到:output/output.mp4 |
85
+ | --device | Option | 运行时的设备,可选择`CPU/GPU/XPU`,默认为`CPU` |
86
+ | --run_mode | Option | 使用GPU时,默认为paddle, 可选(paddle/trt_fp32/trt_fp16/trt_int8) |
87
+ | --batch_size | Option | 预测时的batch size,在指定`image_dir`时有效,默认为1 |
88
+ | --threshold | Option| 预测得分的阈值,默认为0.5 |
89
+ | --output_dir | Option| 可视化结果保存的根目录,默认为output/ |
90
+ | --run_benchmark | Option| 是否运行benchmark,同时需指定`--image_file`或`--image_dir`,默认为False |
91
+ | --enable_mkldnn | Option | CPU预测中是否开启MKLDNN加速,默认为False |
92
+ | --cpu_threads | Option| 设置cpu线程数,默认为1 |
93
+ | --trt_calib_mode | Option| TensorRT是否使用校准功能,默认为False。使用TensorRT的int8功能时,需设置为True,使用PaddleSlim量化后的模型时需要设置为False |
94
+ | --save_images | Option| 是否保存可视化结果 |
95
+ | --save_results | Option| 是否在文件夹下将图片的预测结果以JSON的形式保存 |
96
+
97
+
98
+ 说明:
99
+
100
+ - 参数优先级顺序:`camera_id` > `video_file` > `image_dir` > `image_file`。
101
+ - run_mode:paddle代表使用AnalysisPredictor,精度float32来推理,其他参数指用AnalysisPredictor,TensorRT不同精度来推理。
102
+ - 如果安装的PaddlePaddle不支持基于TensorRT进行预测,需要自行编译,详细可参考[预测库编译教程](https://paddleinference.paddlepaddle.org.cn/user_guides/source_compile.html)。
103
+ - --run_benchmark如果设置为True,则需要安装依赖`pip install pynvml psutil GPUtil`。
104
+ - 如果需要使用导出模型在coco数据集上进行评估,请在推理时添加`--save_results`和`--use_coco_category`参数用以保存coco评估所需要的json文件
python/benchmark_utils.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import logging
17
+
18
+ import paddle
19
+ import paddle.inference as paddle_infer
20
+
21
+ from pathlib import Path
22
+
23
+ CUR_DIR = os.path.dirname(os.path.abspath(__file__))
24
+ LOG_PATH_ROOT = f"{CUR_DIR}/../../output"
25
+
26
+
27
+ class PaddleInferBenchmark(object):
28
+ def __init__(self,
29
+ config,
30
+ model_info: dict={},
31
+ data_info: dict={},
32
+ perf_info: dict={},
33
+ resource_info: dict={},
34
+ **kwargs):
35
+ """
36
+ Construct PaddleInferBenchmark Class to format logs.
37
+ args:
38
+ config(paddle.inference.Config): paddle inference config
39
+ model_info(dict): basic model info
40
+ {'model_name': 'resnet50'
41
+ 'precision': 'fp32'}
42
+ data_info(dict): input data info
43
+ {'batch_size': 1
44
+ 'shape': '3,224,224'
45
+ 'data_num': 1000}
46
+ perf_info(dict): performance result
47
+ {'preprocess_time_s': 1.0
48
+ 'inference_time_s': 2.0
49
+ 'postprocess_time_s': 1.0
50
+ 'total_time_s': 4.0}
51
+ resource_info(dict):
52
+ cpu and gpu resources
53
+ {'cpu_rss': 100
54
+ 'gpu_rss': 100
55
+ 'gpu_util': 60}
56
+ """
57
+ # PaddleInferBenchmark Log Version
58
+ self.log_version = "1.0.3"
59
+
60
+ # Paddle Version
61
+ self.paddle_version = paddle.__version__
62
+ self.paddle_commit = paddle.__git_commit__
63
+ paddle_infer_info = paddle_infer.get_version()
64
+ self.paddle_branch = paddle_infer_info.strip().split(': ')[-1]
65
+
66
+ # model info
67
+ self.model_info = model_info
68
+
69
+ # data info
70
+ self.data_info = data_info
71
+
72
+ # perf info
73
+ self.perf_info = perf_info
74
+
75
+ try:
76
+ # required value
77
+ self.model_name = model_info['model_name']
78
+ self.precision = model_info['precision']
79
+
80
+ self.batch_size = data_info['batch_size']
81
+ self.shape = data_info['shape']
82
+ self.data_num = data_info['data_num']
83
+
84
+ self.inference_time_s = round(perf_info['inference_time_s'], 4)
85
+ except:
86
+ self.print_help()
87
+ raise ValueError(
88
+ "Set argument wrong, please check input argument and its type")
89
+
90
+ self.preprocess_time_s = perf_info.get('preprocess_time_s', 0)
91
+ self.postprocess_time_s = perf_info.get('postprocess_time_s', 0)
92
+ self.with_tracker = True if 'tracking_time_s' in perf_info else False
93
+ self.tracking_time_s = perf_info.get('tracking_time_s', 0)
94
+ self.total_time_s = perf_info.get('total_time_s', 0)
95
+
96
+ self.inference_time_s_90 = perf_info.get("inference_time_s_90", "")
97
+ self.inference_time_s_99 = perf_info.get("inference_time_s_99", "")
98
+ self.succ_rate = perf_info.get("succ_rate", "")
99
+ self.qps = perf_info.get("qps", "")
100
+
101
+ # conf info
102
+ self.config_status = self.parse_config(config)
103
+
104
+ # mem info
105
+ if isinstance(resource_info, dict):
106
+ self.cpu_rss_mb = int(resource_info.get('cpu_rss_mb', 0))
107
+ self.cpu_vms_mb = int(resource_info.get('cpu_vms_mb', 0))
108
+ self.cpu_shared_mb = int(resource_info.get('cpu_shared_mb', 0))
109
+ self.cpu_dirty_mb = int(resource_info.get('cpu_dirty_mb', 0))
110
+ self.cpu_util = round(resource_info.get('cpu_util', 0), 2)
111
+
112
+ self.gpu_rss_mb = int(resource_info.get('gpu_rss_mb', 0))
113
+ self.gpu_util = round(resource_info.get('gpu_util', 0), 2)
114
+ self.gpu_mem_util = round(resource_info.get('gpu_mem_util', 0), 2)
115
+ else:
116
+ self.cpu_rss_mb = 0
117
+ self.cpu_vms_mb = 0
118
+ self.cpu_shared_mb = 0
119
+ self.cpu_dirty_mb = 0
120
+ self.cpu_util = 0
121
+
122
+ self.gpu_rss_mb = 0
123
+ self.gpu_util = 0
124
+ self.gpu_mem_util = 0
125
+
126
+ # init benchmark logger
127
+ self.benchmark_logger()
128
+
129
+ def benchmark_logger(self):
130
+ """
131
+ benchmark logger
132
+ """
133
+ # remove other logging handler
134
+ for handler in logging.root.handlers[:]:
135
+ logging.root.removeHandler(handler)
136
+
137
+ # Init logger
138
+ FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
139
+ log_output = f"{LOG_PATH_ROOT}/{self.model_name}.log"
140
+ Path(f"{LOG_PATH_ROOT}").mkdir(parents=True, exist_ok=True)
141
+ logging.basicConfig(
142
+ level=logging.INFO,
143
+ format=FORMAT,
144
+ handlers=[
145
+ logging.FileHandler(
146
+ filename=log_output, mode='w'),
147
+ logging.StreamHandler(),
148
+ ])
149
+ self.logger = logging.getLogger(__name__)
150
+ self.logger.info(
151
+ f"Paddle Inference benchmark log will be saved to {log_output}")
152
+
153
+ def parse_config(self, config) -> dict:
154
+ """
155
+ parse paddle predictor config
156
+ args:
157
+ config(paddle.inference.Config): paddle inference config
158
+ return:
159
+ config_status(dict): dict style config info
160
+ """
161
+ if isinstance(config, paddle_infer.Config):
162
+ config_status = {}
163
+ config_status['runtime_device'] = "gpu" if config.use_gpu(
164
+ ) else "cpu"
165
+ config_status['ir_optim'] = config.ir_optim()
166
+ config_status['enable_tensorrt'] = config.tensorrt_engine_enabled()
167
+ config_status['precision'] = self.precision
168
+ config_status['enable_mkldnn'] = config.mkldnn_enabled()
169
+ config_status[
170
+ 'cpu_math_library_num_threads'] = config.cpu_math_library_num_threads(
171
+ )
172
+ elif isinstance(config, dict):
173
+ config_status['runtime_device'] = config.get('runtime_device', "")
174
+ config_status['ir_optim'] = config.get('ir_optim', "")
175
+ config_status['enable_tensorrt'] = config.get('enable_tensorrt',
176
+ "")
177
+ config_status['precision'] = config.get('precision', "")
178
+ config_status['enable_mkldnn'] = config.get('enable_mkldnn', "")
179
+ config_status['cpu_math_library_num_threads'] = config.get(
180
+ 'cpu_math_library_num_threads', "")
181
+ else:
182
+ self.print_help()
183
+ raise ValueError(
184
+ "Set argument config wrong, please check input argument and its type"
185
+ )
186
+ return config_status
187
+
188
+ def report(self, identifier=None):
189
+ """
190
+ print log report
191
+ args:
192
+ identifier(string): identify log
193
+ """
194
+ if identifier:
195
+ identifier = f"[{identifier}]"
196
+ else:
197
+ identifier = ""
198
+
199
+ self.logger.info("\n")
200
+ self.logger.info(
201
+ "---------------------- Paddle info ----------------------")
202
+ self.logger.info(f"{identifier} paddle_version: {self.paddle_version}")
203
+ self.logger.info(f"{identifier} paddle_commit: {self.paddle_commit}")
204
+ self.logger.info(f"{identifier} paddle_branch: {self.paddle_branch}")
205
+ self.logger.info(f"{identifier} log_api_version: {self.log_version}")
206
+ self.logger.info(
207
+ "----------------------- Conf info -----------------------")
208
+ self.logger.info(
209
+ f"{identifier} runtime_device: {self.config_status['runtime_device']}"
210
+ )
211
+ self.logger.info(
212
+ f"{identifier} ir_optim: {self.config_status['ir_optim']}")
213
+ self.logger.info(f"{identifier} enable_memory_optim: {True}")
214
+ self.logger.info(
215
+ f"{identifier} enable_tensorrt: {self.config_status['enable_tensorrt']}"
216
+ )
217
+ self.logger.info(
218
+ f"{identifier} enable_mkldnn: {self.config_status['enable_mkldnn']}"
219
+ )
220
+ self.logger.info(
221
+ f"{identifier} cpu_math_library_num_threads: {self.config_status['cpu_math_library_num_threads']}"
222
+ )
223
+ self.logger.info(
224
+ "----------------------- Model info ----------------------")
225
+ self.logger.info(f"{identifier} model_name: {self.model_name}")
226
+ self.logger.info(f"{identifier} precision: {self.precision}")
227
+ self.logger.info(
228
+ "----------------------- Data info -----------------------")
229
+ self.logger.info(f"{identifier} batch_size: {self.batch_size}")
230
+ self.logger.info(f"{identifier} input_shape: {self.shape}")
231
+ self.logger.info(f"{identifier} data_num: {self.data_num}")
232
+ self.logger.info(
233
+ "----------------------- Perf info -----------------------")
234
+ self.logger.info(
235
+ f"{identifier} cpu_rss(MB): {self.cpu_rss_mb}, cpu_vms: {self.cpu_vms_mb}, cpu_shared_mb: {self.cpu_shared_mb}, cpu_dirty_mb: {self.cpu_dirty_mb}, cpu_util: {self.cpu_util}%"
236
+ )
237
+ self.logger.info(
238
+ f"{identifier} gpu_rss(MB): {self.gpu_rss_mb}, gpu_util: {self.gpu_util}%, gpu_mem_util: {self.gpu_mem_util}%"
239
+ )
240
+ self.logger.info(
241
+ f"{identifier} total time spent(s): {self.total_time_s}")
242
+
243
+ if self.with_tracker:
244
+ self.logger.info(
245
+ f"{identifier} preprocess_time(ms): {round(self.preprocess_time_s*1000, 1)}, "
246
+ f"inference_time(ms): {round(self.inference_time_s*1000, 1)}, "
247
+ f"postprocess_time(ms): {round(self.postprocess_time_s*1000, 1)}, "
248
+ f"tracking_time(ms): {round(self.tracking_time_s*1000, 1)}")
249
+ else:
250
+ self.logger.info(
251
+ f"{identifier} preprocess_time(ms): {round(self.preprocess_time_s*1000, 1)}, "
252
+ f"inference_time(ms): {round(self.inference_time_s*1000, 1)}, "
253
+ f"postprocess_time(ms): {round(self.postprocess_time_s*1000, 1)}"
254
+ )
255
+ if self.inference_time_s_90:
256
+ self.looger.info(
257
+ f"{identifier} 90%_cost: {self.inference_time_s_90}, 99%_cost: {self.inference_time_s_99}, succ_rate: {self.succ_rate}"
258
+ )
259
+ if self.qps:
260
+ self.logger.info(f"{identifier} QPS: {self.qps}")
261
+
262
+ def print_help(self):
263
+ """
264
+ print function help
265
+ """
266
+ print("""Usage:
267
+ ==== Print inference benchmark logs. ====
268
+ config = paddle.inference.Config()
269
+ model_info = {'model_name': 'resnet50'
270
+ 'precision': 'fp32'}
271
+ data_info = {'batch_size': 1
272
+ 'shape': '3,224,224'
273
+ 'data_num': 1000}
274
+ perf_info = {'preprocess_time_s': 1.0
275
+ 'inference_time_s': 2.0
276
+ 'postprocess_time_s': 1.0
277
+ 'total_time_s': 4.0}
278
+ resource_info = {'cpu_rss_mb': 100
279
+ 'gpu_rss_mb': 100
280
+ 'gpu_util': 60}
281
+ log = PaddleInferBenchmark(config, model_info, data_info, perf_info, resource_info)
282
+ log('Test')
283
+ """)
284
+
285
+ def __call__(self, identifier=None):
286
+ """
287
+ __call__
288
+ args:
289
+ identifier(string): identify log
290
+ """
291
+ self.report(identifier)
python/det_keypoint_unite_infer.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import json
17
+ import cv2
18
+ import math
19
+ import numpy as np
20
+ import paddle
21
+ import yaml
22
+
23
+ from det_keypoint_unite_utils import argsparser
24
+ from preprocess import decode_image
25
+ from infer import Detector, DetectorPicoDet, PredictConfig, print_arguments, get_test_images, bench_log
26
+ from keypoint_infer import KeyPointDetector, PredictConfig_KeyPoint
27
+ from visualize import visualize_pose
28
+ from benchmark_utils import PaddleInferBenchmark
29
+ from utils import get_current_memory_mb
30
+ from keypoint_postprocess import translate_to_ori_images
31
+
32
+ KEYPOINT_SUPPORT_MODELS = {
33
+ 'HigherHRNet': 'keypoint_bottomup',
34
+ 'HRNet': 'keypoint_topdown'
35
+ }
36
+
37
+
38
+ def predict_with_given_det(image, det_res, keypoint_detector,
39
+ keypoint_batch_size, run_benchmark):
40
+ keypoint_res = {}
41
+
42
+ rec_images, records, det_rects = keypoint_detector.get_person_from_rect(
43
+ image, det_res)
44
+
45
+ if len(det_rects) == 0:
46
+ keypoint_res['keypoint'] = [[], []]
47
+ return keypoint_res
48
+
49
+ keypoint_vector = []
50
+ score_vector = []
51
+
52
+ rect_vector = det_rects
53
+ keypoint_results = keypoint_detector.predict_image(
54
+ rec_images, run_benchmark, repeats=10, visual=False)
55
+ keypoint_vector, score_vector = translate_to_ori_images(keypoint_results,
56
+ np.array(records))
57
+ keypoint_res['keypoint'] = [
58
+ keypoint_vector.tolist(), score_vector.tolist()
59
+ ] if len(keypoint_vector) > 0 else [[], []]
60
+ keypoint_res['bbox'] = rect_vector
61
+ return keypoint_res
62
+
63
+
64
+ def topdown_unite_predict(detector,
65
+ topdown_keypoint_detector,
66
+ image_list,
67
+ keypoint_batch_size=1,
68
+ save_res=False):
69
+ det_timer = detector.get_timer()
70
+ store_res = []
71
+ for i, img_file in enumerate(image_list):
72
+ # Decode image in advance in det + pose prediction
73
+ det_timer.preprocess_time_s.start()
74
+ image, _ = decode_image(img_file, {})
75
+ det_timer.preprocess_time_s.end()
76
+
77
+ if FLAGS.run_benchmark:
78
+ results = detector.predict_image(
79
+ [image], run_benchmark=True, repeats=10)
80
+
81
+ cm, gm, gu = get_current_memory_mb()
82
+ detector.cpu_mem += cm
83
+ detector.gpu_mem += gm
84
+ detector.gpu_util += gu
85
+ else:
86
+ results = detector.predict_image([image], visual=False)
87
+ results = detector.filter_box(results, FLAGS.det_threshold)
88
+ if results['boxes_num'] > 0:
89
+ keypoint_res = predict_with_given_det(
90
+ image, results, topdown_keypoint_detector, keypoint_batch_size,
91
+ FLAGS.run_benchmark)
92
+
93
+ if save_res:
94
+ save_name = img_file if isinstance(img_file, str) else i
95
+ store_res.append([
96
+ save_name, keypoint_res['bbox'], [
97
+ keypoint_res['keypoint'][0],
98
+ keypoint_res['keypoint'][1]
99
+ ]
100
+ ])
101
+ else:
102
+ results["keypoint"] = [[], []]
103
+ keypoint_res = results
104
+ if FLAGS.run_benchmark:
105
+ cm, gm, gu = get_current_memory_mb()
106
+ topdown_keypoint_detector.cpu_mem += cm
107
+ topdown_keypoint_detector.gpu_mem += gm
108
+ topdown_keypoint_detector.gpu_util += gu
109
+ else:
110
+ if not os.path.exists(FLAGS.output_dir):
111
+ os.makedirs(FLAGS.output_dir)
112
+ visualize_pose(
113
+ img_file,
114
+ keypoint_res,
115
+ visual_thresh=FLAGS.keypoint_threshold,
116
+ save_dir=FLAGS.output_dir)
117
+ if save_res:
118
+ """
119
+ 1) store_res: a list of image_data
120
+ 2) image_data: [imageid, rects, [keypoints, scores]]
121
+ 3) rects: list of rect [xmin, ymin, xmax, ymax]
122
+ 4) keypoints: 17(joint numbers)*[x, y, conf], total 51 data in list
123
+ 5) scores: mean of all joint conf
124
+ """
125
+ with open("det_keypoint_unite_image_results.json", 'w') as wf:
126
+ json.dump(store_res, wf, indent=4)
127
+
128
+
129
+ def topdown_unite_predict_video(detector,
130
+ topdown_keypoint_detector,
131
+ camera_id,
132
+ keypoint_batch_size=1,
133
+ save_res=False):
134
+ video_name = 'output.mp4'
135
+ if camera_id != -1:
136
+ capture = cv2.VideoCapture(camera_id)
137
+ else:
138
+ capture = cv2.VideoCapture(FLAGS.video_file)
139
+ video_name = os.path.split(FLAGS.video_file)[-1]
140
+ # Get Video info : resolution, fps, frame count
141
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
142
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
143
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
144
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
145
+ print("fps: %d, frame_count: %d" % (fps, frame_count))
146
+
147
+ if not os.path.exists(FLAGS.output_dir):
148
+ os.makedirs(FLAGS.output_dir)
149
+ out_path = os.path.join(FLAGS.output_dir, video_name)
150
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
151
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
152
+ index = 0
153
+ store_res = []
154
+ keypoint_smoothing = KeypointSmoothing(
155
+ width, height, filter_type=FLAGS.filter_type, beta=0.05)
156
+
157
+ while (1):
158
+ ret, frame = capture.read()
159
+ if not ret:
160
+ break
161
+ index += 1
162
+ print('detect frame: %d' % (index))
163
+
164
+ frame2 = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
165
+
166
+ results = detector.predict_image([frame2], visual=False)
167
+ results = detector.filter_box(results, FLAGS.det_threshold)
168
+ if results['boxes_num'] == 0:
169
+ writer.write(frame)
170
+ continue
171
+
172
+ keypoint_res = predict_with_given_det(
173
+ frame2, results, topdown_keypoint_detector, keypoint_batch_size,
174
+ FLAGS.run_benchmark)
175
+
176
+ if FLAGS.smooth and len(keypoint_res['keypoint'][0]) == 1:
177
+ current_keypoints = np.array(keypoint_res['keypoint'][0][0])
178
+ smooth_keypoints = keypoint_smoothing.smooth_process(
179
+ current_keypoints)
180
+
181
+ keypoint_res['keypoint'][0][0] = smooth_keypoints.tolist()
182
+
183
+ im = visualize_pose(
184
+ frame,
185
+ keypoint_res,
186
+ visual_thresh=FLAGS.keypoint_threshold,
187
+ returnimg=True)
188
+
189
+ if save_res:
190
+ store_res.append([
191
+ index, keypoint_res['bbox'],
192
+ [keypoint_res['keypoint'][0], keypoint_res['keypoint'][1]]
193
+ ])
194
+
195
+ writer.write(im)
196
+ if camera_id != -1:
197
+ cv2.imshow('Mask Detection', im)
198
+ if cv2.waitKey(1) & 0xFF == ord('q'):
199
+ break
200
+ writer.release()
201
+ print('output_video saved to: {}'.format(out_path))
202
+ if save_res:
203
+ """
204
+ 1) store_res: a list of frame_data
205
+ 2) frame_data: [frameid, rects, [keypoints, scores]]
206
+ 3) rects: list of rect [xmin, ymin, xmax, ymax]
207
+ 4) keypoints: 17(joint numbers)*[x, y, conf], total 51 data in list
208
+ 5) scores: mean of all joint conf
209
+ """
210
+ with open("det_keypoint_unite_video_results.json", 'w') as wf:
211
+ json.dump(store_res, wf, indent=4)
212
+
213
+
214
+ class KeypointSmoothing(object):
215
+ # The following code are modified from:
216
+ # https://github.com/jaantollander/OneEuroFilter
217
+
218
+ def __init__(self,
219
+ width,
220
+ height,
221
+ filter_type,
222
+ alpha=0.5,
223
+ fc_d=0.1,
224
+ fc_min=0.1,
225
+ beta=0.1,
226
+ thres_mult=0.3):
227
+ super(KeypointSmoothing, self).__init__()
228
+ self.image_width = width
229
+ self.image_height = height
230
+ self.threshold = np.array([
231
+ 0.005, 0.005, 0.005, 0.005, 0.005, 0.01, 0.01, 0.01, 0.01, 0.01,
232
+ 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01
233
+ ]) * thres_mult
234
+ self.filter_type = filter_type
235
+ self.alpha = alpha
236
+ self.dx_prev_hat = None
237
+ self.x_prev_hat = None
238
+ self.fc_d = fc_d
239
+ self.fc_min = fc_min
240
+ self.beta = beta
241
+
242
+ if self.filter_type == 'OneEuro':
243
+ self.smooth_func = self.one_euro_filter
244
+ elif self.filter_type == 'EMA':
245
+ self.smooth_func = self.ema_filter
246
+ else:
247
+ raise ValueError('filter type must be one_euro or ema')
248
+
249
+ def smooth_process(self, current_keypoints):
250
+ if self.x_prev_hat is None:
251
+ self.x_prev_hat = current_keypoints[:, :2]
252
+ self.dx_prev_hat = np.zeros(current_keypoints[:, :2].shape)
253
+ return current_keypoints
254
+ else:
255
+ result = current_keypoints
256
+ num_keypoints = len(current_keypoints)
257
+ for i in range(num_keypoints):
258
+ result[i, :2] = self.smooth(current_keypoints[i, :2],
259
+ self.threshold[i], i)
260
+ return result
261
+
262
+ def smooth(self, current_keypoint, threshold, index):
263
+ distance = np.sqrt(
264
+ np.square((current_keypoint[0] - self.x_prev_hat[index][0]) /
265
+ self.image_width) + np.square((current_keypoint[
266
+ 1] - self.x_prev_hat[index][1]) / self.image_height))
267
+ if distance < threshold:
268
+ result = self.x_prev_hat[index]
269
+ else:
270
+ result = self.smooth_func(current_keypoint, self.x_prev_hat[index],
271
+ index)
272
+
273
+ return result
274
+
275
+ def one_euro_filter(self, x_cur, x_pre, index):
276
+ te = 1
277
+ self.alpha = self.smoothing_factor(te, self.fc_d)
278
+ dx_cur = (x_cur - x_pre) / te
279
+ dx_cur_hat = self.exponential_smoothing(dx_cur,
280
+ self.dx_prev_hat[index])
281
+
282
+ fc = self.fc_min + self.beta * np.abs(dx_cur_hat)
283
+ self.alpha = self.smoothing_factor(te, fc)
284
+ x_cur_hat = self.exponential_smoothing(x_cur, x_pre)
285
+ self.dx_prev_hat[index] = dx_cur_hat
286
+ self.x_prev_hat[index] = x_cur_hat
287
+ return x_cur_hat
288
+
289
+ def ema_filter(self, x_cur, x_pre, index):
290
+ x_cur_hat = self.exponential_smoothing(x_cur, x_pre)
291
+ self.x_prev_hat[index] = x_cur_hat
292
+ return x_cur_hat
293
+
294
+ def smoothing_factor(self, te, fc):
295
+ r = 2 * math.pi * fc * te
296
+ return r / (r + 1)
297
+
298
+ def exponential_smoothing(self, x_cur, x_pre, index=0):
299
+ return self.alpha * x_cur + (1 - self.alpha) * x_pre
300
+
301
+
302
+ def main():
303
+ deploy_file = os.path.join(FLAGS.det_model_dir, 'infer_cfg.yml')
304
+ with open(deploy_file) as f:
305
+ yml_conf = yaml.safe_load(f)
306
+ arch = yml_conf['arch']
307
+ detector_func = 'Detector'
308
+ if arch == 'PicoDet':
309
+ detector_func = 'DetectorPicoDet'
310
+
311
+ detector = eval(detector_func)(FLAGS.det_model_dir,
312
+ device=FLAGS.device,
313
+ run_mode=FLAGS.run_mode,
314
+ trt_min_shape=FLAGS.trt_min_shape,
315
+ trt_max_shape=FLAGS.trt_max_shape,
316
+ trt_opt_shape=FLAGS.trt_opt_shape,
317
+ trt_calib_mode=FLAGS.trt_calib_mode,
318
+ cpu_threads=FLAGS.cpu_threads,
319
+ enable_mkldnn=FLAGS.enable_mkldnn,
320
+ threshold=FLAGS.det_threshold)
321
+
322
+ topdown_keypoint_detector = KeyPointDetector(
323
+ FLAGS.keypoint_model_dir,
324
+ device=FLAGS.device,
325
+ run_mode=FLAGS.run_mode,
326
+ batch_size=FLAGS.keypoint_batch_size,
327
+ trt_min_shape=FLAGS.trt_min_shape,
328
+ trt_max_shape=FLAGS.trt_max_shape,
329
+ trt_opt_shape=FLAGS.trt_opt_shape,
330
+ trt_calib_mode=FLAGS.trt_calib_mode,
331
+ cpu_threads=FLAGS.cpu_threads,
332
+ enable_mkldnn=FLAGS.enable_mkldnn,
333
+ use_dark=FLAGS.use_dark)
334
+ keypoint_arch = topdown_keypoint_detector.pred_config.arch
335
+ assert KEYPOINT_SUPPORT_MODELS[
336
+ keypoint_arch] == 'keypoint_topdown', 'Detection-Keypoint unite inference only supports topdown models.'
337
+
338
+ # predict from video file or camera video stream
339
+ if FLAGS.video_file is not None or FLAGS.camera_id != -1:
340
+ topdown_unite_predict_video(detector, topdown_keypoint_detector,
341
+ FLAGS.camera_id, FLAGS.keypoint_batch_size,
342
+ FLAGS.save_res)
343
+ else:
344
+ # predict from image
345
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
346
+ topdown_unite_predict(detector, topdown_keypoint_detector, img_list,
347
+ FLAGS.keypoint_batch_size, FLAGS.save_res)
348
+ if not FLAGS.run_benchmark:
349
+ detector.det_times.info(average=True)
350
+ topdown_keypoint_detector.det_times.info(average=True)
351
+ else:
352
+ mode = FLAGS.run_mode
353
+ det_model_dir = FLAGS.det_model_dir
354
+ det_model_info = {
355
+ 'model_name': det_model_dir.strip('/').split('/')[-1],
356
+ 'precision': mode.split('_')[-1]
357
+ }
358
+ bench_log(detector, img_list, det_model_info, name='Det')
359
+ keypoint_model_dir = FLAGS.keypoint_model_dir
360
+ keypoint_model_info = {
361
+ 'model_name': keypoint_model_dir.strip('/').split('/')[-1],
362
+ 'precision': mode.split('_')[-1]
363
+ }
364
+ bench_log(topdown_keypoint_detector, img_list, keypoint_model_info,
365
+ FLAGS.keypoint_batch_size, 'KeyPoint')
366
+
367
+
368
+ if __name__ == '__main__':
369
+ paddle.enable_static()
370
+ parser = argsparser()
371
+ FLAGS = parser.parse_args()
372
+ print_arguments(FLAGS)
373
+ FLAGS.device = FLAGS.device.upper()
374
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
375
+ ], "device should be CPU, GPU or XPU"
376
+
377
+ main()
python/det_keypoint_unite_utils.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import ast
16
+ import argparse
17
+
18
+
19
+ def argsparser():
20
+ parser = argparse.ArgumentParser(description=__doc__)
21
+ parser.add_argument(
22
+ "--det_model_dir",
23
+ type=str,
24
+ default=None,
25
+ help=("Directory include:'model.pdiparams', 'model.pdmodel', "
26
+ "'infer_cfg.yml', created by tools/export_model.py."),
27
+ required=True)
28
+ parser.add_argument(
29
+ "--keypoint_model_dir",
30
+ type=str,
31
+ default=None,
32
+ help=("Directory include:'model.pdiparams', 'model.pdmodel', "
33
+ "'infer_cfg.yml', created by tools/export_model.py."),
34
+ required=True)
35
+ parser.add_argument(
36
+ "--image_file", type=str, default=None, help="Path of image file.")
37
+ parser.add_argument(
38
+ "--image_dir",
39
+ type=str,
40
+ default=None,
41
+ help="Dir of image file, `image_file` has a higher priority.")
42
+ parser.add_argument(
43
+ "--keypoint_batch_size",
44
+ type=int,
45
+ default=8,
46
+ help=("batch_size for keypoint inference. In detection-keypoint unit"
47
+ "inference, the batch size in detection is 1. Then collate det "
48
+ "result in batch for keypoint inference."))
49
+ parser.add_argument(
50
+ "--video_file",
51
+ type=str,
52
+ default=None,
53
+ help="Path of video file, `video_file` or `camera_id` has a highest priority."
54
+ )
55
+ parser.add_argument(
56
+ "--camera_id",
57
+ type=int,
58
+ default=-1,
59
+ help="device id of camera to predict.")
60
+ parser.add_argument(
61
+ "--det_threshold", type=float, default=0.5, help="Threshold of score.")
62
+ parser.add_argument(
63
+ "--keypoint_threshold",
64
+ type=float,
65
+ default=0.5,
66
+ help="Threshold of score.")
67
+ parser.add_argument(
68
+ "--output_dir",
69
+ type=str,
70
+ default="output",
71
+ help="Directory of output visualization files.")
72
+ parser.add_argument(
73
+ "--run_mode",
74
+ type=str,
75
+ default='paddle',
76
+ help="mode of running(paddle/trt_fp32/trt_fp16/trt_int8)")
77
+ parser.add_argument(
78
+ "--device",
79
+ type=str,
80
+ default='cpu',
81
+ help="Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU."
82
+ )
83
+ parser.add_argument(
84
+ "--run_benchmark",
85
+ type=ast.literal_eval,
86
+ default=False,
87
+ help="Whether to predict a image_file repeatedly for benchmark")
88
+ parser.add_argument(
89
+ "--enable_mkldnn",
90
+ type=ast.literal_eval,
91
+ default=False,
92
+ help="Whether use mkldnn with CPU.")
93
+ parser.add_argument(
94
+ "--cpu_threads", type=int, default=1, help="Num of threads with CPU.")
95
+ parser.add_argument(
96
+ "--trt_min_shape", type=int, default=1, help="min_shape for TensorRT.")
97
+ parser.add_argument(
98
+ "--trt_max_shape",
99
+ type=int,
100
+ default=1280,
101
+ help="max_shape for TensorRT.")
102
+ parser.add_argument(
103
+ "--trt_opt_shape",
104
+ type=int,
105
+ default=640,
106
+ help="opt_shape for TensorRT.")
107
+ parser.add_argument(
108
+ "--trt_calib_mode",
109
+ type=bool,
110
+ default=False,
111
+ help="If the model is produced by TRT offline quantitative "
112
+ "calibration, trt_calib_mode need to set True.")
113
+ parser.add_argument(
114
+ '--use_dark',
115
+ type=ast.literal_eval,
116
+ default=True,
117
+ help='whether to use darkpose to get better keypoint position predict ')
118
+ parser.add_argument(
119
+ '--save_res',
120
+ type=bool,
121
+ default=False,
122
+ help=(
123
+ "whether to save predict results to json file"
124
+ "1) store_res: a list of image_data"
125
+ "2) image_data: [imageid, rects, [keypoints, scores]]"
126
+ "3) rects: list of rect [xmin, ymin, xmax, ymax]"
127
+ "4) keypoints: 17(joint numbers)*[x, y, conf], total 51 data in list"
128
+ "5) scores: mean of all joint conf"))
129
+ parser.add_argument(
130
+ '--smooth',
131
+ type=ast.literal_eval,
132
+ default=False,
133
+ help='smoothing keypoints for each frame, new incoming keypoints will be more stable.'
134
+ )
135
+ parser.add_argument(
136
+ '--filter_type',
137
+ type=str,
138
+ default='OneEuro',
139
+ help='when set --smooth True, choose filter type you want to use, it can be [OneEuro] or [EMA].'
140
+ )
141
+ return parser
python/infer.py ADDED
@@ -0,0 +1,1035 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import yaml
17
+ import glob
18
+ import json
19
+ from pathlib import Path
20
+ from functools import reduce
21
+
22
+ import cv2
23
+ import numpy as np
24
+ import math
25
+ import paddle
26
+ from paddle.inference import Config
27
+ from paddle.inference import create_predictor
28
+
29
+ import sys
30
+ # add deploy path of PadleDetection to sys.path
31
+ parent_path = os.path.abspath(os.path.join(__file__, *(['..'])))
32
+ sys.path.insert(0, parent_path)
33
+
34
+ from benchmark_utils import PaddleInferBenchmark
35
+ from picodet_postprocess import PicoDetPostProcess
36
+ from preprocess import preprocess, Resize, NormalizeImage, Permute, PadStride, LetterBoxResize, WarpAffine, Pad, decode_image
37
+ from keypoint_preprocess import EvalAffine, TopDownEvalAffine, expand_crop
38
+ from visualize import visualize_box_mask
39
+ from utils import argsparser, Timer, get_current_memory_mb, multiclass_nms, coco_clsid2catid
40
+
41
+ # Global dictionary
42
+ SUPPORT_MODELS = {
43
+ 'YOLO', 'RCNN', 'SSD', 'Face', 'FCOS', 'SOLOv2', 'TTFNet', 'S2ANet', 'JDE',
44
+ 'FairMOT', 'DeepSORT', 'GFL', 'PicoDet', 'CenterNet', 'TOOD', 'RetinaNet',
45
+ 'StrongBaseline', 'STGCN', 'YOLOX', 'PPHGNet', 'PPLCNet'
46
+ }
47
+
48
+
49
+ def bench_log(detector, img_list, model_info, batch_size=1, name=None):
50
+ mems = {
51
+ 'cpu_rss_mb': detector.cpu_mem / len(img_list),
52
+ 'gpu_rss_mb': detector.gpu_mem / len(img_list),
53
+ 'gpu_util': detector.gpu_util * 100 / len(img_list)
54
+ }
55
+ perf_info = detector.det_times.report(average=True)
56
+ data_info = {
57
+ 'batch_size': batch_size,
58
+ 'shape': "dynamic_shape",
59
+ 'data_num': perf_info['img_num']
60
+ }
61
+ log = PaddleInferBenchmark(detector.config, model_info, data_info,
62
+ perf_info, mems)
63
+ log(name)
64
+
65
+
66
+ class Detector(object):
67
+ """
68
+ Args:
69
+ pred_config (object): config of model, defined by `Config(model_dir)`
70
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
71
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
72
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
73
+ batch_size (int): size of pre batch in inference
74
+ trt_min_shape (int): min shape for dynamic shape in trt
75
+ trt_max_shape (int): max shape for dynamic shape in trt
76
+ trt_opt_shape (int): opt shape for dynamic shape in trt
77
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
78
+ calibration, trt_calib_mode need to set True
79
+ cpu_threads (int): cpu threads
80
+ enable_mkldnn (bool): whether to open MKLDNN
81
+ enable_mkldnn_bfloat16 (bool): whether to turn on mkldnn bfloat16
82
+ output_dir (str): The path of output
83
+ threshold (float): The threshold of score for visualization
84
+ delete_shuffle_pass (bool): whether to remove shuffle_channel_detect_pass in TensorRT.
85
+ Used by action model.
86
+ """
87
+
88
+ def __init__(self,
89
+ model_dir,
90
+ device='CPU',
91
+ run_mode='paddle',
92
+ batch_size=1,
93
+ trt_min_shape=1,
94
+ trt_max_shape=1280,
95
+ trt_opt_shape=640,
96
+ trt_calib_mode=False,
97
+ cpu_threads=1,
98
+ enable_mkldnn=False,
99
+ enable_mkldnn_bfloat16=False,
100
+ output_dir='output',
101
+ threshold=0.5,
102
+ delete_shuffle_pass=False):
103
+ self.pred_config = self.set_config(model_dir)
104
+ self.predictor, self.config = load_predictor(
105
+ model_dir,
106
+ run_mode=run_mode,
107
+ batch_size=batch_size,
108
+ min_subgraph_size=self.pred_config.min_subgraph_size,
109
+ device=device,
110
+ use_dynamic_shape=self.pred_config.use_dynamic_shape,
111
+ trt_min_shape=trt_min_shape,
112
+ trt_max_shape=trt_max_shape,
113
+ trt_opt_shape=trt_opt_shape,
114
+ trt_calib_mode=trt_calib_mode,
115
+ cpu_threads=cpu_threads,
116
+ enable_mkldnn=enable_mkldnn,
117
+ enable_mkldnn_bfloat16=enable_mkldnn_bfloat16,
118
+ delete_shuffle_pass=delete_shuffle_pass)
119
+ self.det_times = Timer()
120
+ self.cpu_mem, self.gpu_mem, self.gpu_util = 0, 0, 0
121
+ self.batch_size = batch_size
122
+ self.output_dir = output_dir
123
+ self.threshold = threshold
124
+
125
+ def set_config(self, model_dir):
126
+ return PredictConfig(model_dir)
127
+
128
+ def preprocess(self, image_list):
129
+ preprocess_ops = []
130
+ for op_info in self.pred_config.preprocess_infos:
131
+ new_op_info = op_info.copy()
132
+ op_type = new_op_info.pop('type')
133
+ preprocess_ops.append(eval(op_type)(**new_op_info))
134
+
135
+ input_im_lst = []
136
+ input_im_info_lst = []
137
+ for im_path in image_list:
138
+ im, im_info = preprocess(im_path, preprocess_ops)
139
+ input_im_lst.append(im)
140
+ input_im_info_lst.append(im_info)
141
+ inputs = create_inputs(input_im_lst, input_im_info_lst)
142
+ input_names = self.predictor.get_input_names()
143
+ for i in range(len(input_names)):
144
+ input_tensor = self.predictor.get_input_handle(input_names[i])
145
+ if input_names[i] == 'x':
146
+ input_tensor.copy_from_cpu(inputs['image'])
147
+ else:
148
+ input_tensor.copy_from_cpu(inputs[input_names[i]])
149
+
150
+ return inputs
151
+
152
+ def postprocess(self, inputs, result):
153
+ # postprocess output of predictor
154
+ np_boxes_num = result['boxes_num']
155
+ assert isinstance(np_boxes_num, np.ndarray), \
156
+ '`np_boxes_num` should be a `numpy.ndarray`'
157
+
158
+ if np_boxes_num.sum() <= 0:
159
+ print('[WARNNING] No object detected.')
160
+ result = {'boxes': np.zeros([0, 6]), 'boxes_num': np_boxes_num}
161
+ result = {k: v for k, v in result.items() if v is not None}
162
+ return result
163
+
164
+ def filter_box(self, result, threshold):
165
+ np_boxes_num = result['boxes_num']
166
+ boxes = result['boxes']
167
+ start_idx = 0
168
+ filter_boxes = []
169
+ filter_num = []
170
+ for i in range(len(np_boxes_num)):
171
+ boxes_num = np_boxes_num[i]
172
+ boxes_i = boxes[start_idx:start_idx + boxes_num, :]
173
+ idx = boxes_i[:, 1] > threshold
174
+ filter_boxes_i = boxes_i[idx, :]
175
+ filter_boxes.append(filter_boxes_i)
176
+ filter_num.append(filter_boxes_i.shape[0])
177
+ start_idx += boxes_num
178
+ boxes = np.concatenate(filter_boxes)
179
+ filter_num = np.array(filter_num)
180
+ filter_res = {'boxes': boxes, 'boxes_num': filter_num}
181
+ return filter_res
182
+
183
+ def predict(self, repeats=1):
184
+ '''
185
+ Args:
186
+ repeats (int): repeats number for prediction
187
+ Returns:
188
+ result (dict): include 'boxes': np.ndarray: shape:[N,6], N: number of box,
189
+ matix element:[class, score, x_min, y_min, x_max, y_max]
190
+ MaskRCNN's result include 'masks': np.ndarray:
191
+ shape: [N, im_h, im_w]
192
+ '''
193
+ # model prediction
194
+ np_boxes_num, np_boxes, np_masks = np.array([0]), None, None
195
+ for i in range(repeats):
196
+ self.predictor.run()
197
+ output_names = self.predictor.get_output_names()
198
+ boxes_tensor = self.predictor.get_output_handle(output_names[0])
199
+ np_boxes = boxes_tensor.copy_to_cpu()
200
+ boxes_num = self.predictor.get_output_handle(output_names[1])
201
+ np_boxes_num = boxes_num.copy_to_cpu()
202
+ if self.pred_config.mask:
203
+ masks_tensor = self.predictor.get_output_handle(output_names[
204
+ 2])
205
+ np_masks = masks_tensor.copy_to_cpu()
206
+ result = dict(boxes=np_boxes, masks=np_masks, boxes_num=np_boxes_num)
207
+ return result
208
+
209
+ def merge_batch_result(self, batch_result):
210
+ if len(batch_result) == 1:
211
+ return batch_result[0]
212
+ res_key = batch_result[0].keys()
213
+ results = {k: [] for k in res_key}
214
+ for res in batch_result:
215
+ for k, v in res.items():
216
+ results[k].append(v)
217
+ for k, v in results.items():
218
+ if k not in ['masks', 'segm']:
219
+ results[k] = np.concatenate(v)
220
+ return results
221
+
222
+ def get_timer(self):
223
+ return self.det_times
224
+
225
+ def predict_image_slice(self,
226
+ img_list,
227
+ slice_size=[640, 640],
228
+ overlap_ratio=[0.25, 0.25],
229
+ combine_method='nms',
230
+ match_threshold=0.6,
231
+ match_metric='ios',
232
+ run_benchmark=False,
233
+ repeats=1,
234
+ visual=True,
235
+ save_results=False):
236
+ # slice infer only support bs=1
237
+ results = []
238
+ try:
239
+ import sahi
240
+ from sahi.slicing import slice_image
241
+ except Exception as e:
242
+ print(
243
+ 'sahi not found, plaese install sahi. '
244
+ 'for example: `pip install sahi`, see https://github.com/obss/sahi.'
245
+ )
246
+ raise e
247
+ num_classes = len(self.pred_config.labels)
248
+ for i in range(len(img_list)):
249
+ ori_image = img_list[i]
250
+ slice_image_result = sahi.slicing.slice_image(
251
+ image=ori_image,
252
+ slice_height=slice_size[0],
253
+ slice_width=slice_size[1],
254
+ overlap_height_ratio=overlap_ratio[0],
255
+ overlap_width_ratio=overlap_ratio[1])
256
+ sub_img_num = len(slice_image_result)
257
+ merged_bboxs = []
258
+ print('sub_img_num', sub_img_num)
259
+
260
+ batch_image_list = [
261
+ slice_image_result.images[_ind] for _ind in range(sub_img_num)
262
+ ]
263
+ if run_benchmark:
264
+ # preprocess
265
+ inputs = self.preprocess(batch_image_list) # warmup
266
+ self.det_times.preprocess_time_s.start()
267
+ inputs = self.preprocess(batch_image_list)
268
+ self.det_times.preprocess_time_s.end()
269
+
270
+ # model prediction
271
+ result = self.predict(repeats=50) # warmup
272
+ self.det_times.inference_time_s.start()
273
+ result = self.predict(repeats=repeats)
274
+ self.det_times.inference_time_s.end(repeats=repeats)
275
+
276
+ # postprocess
277
+ result_warmup = self.postprocess(inputs, result) # warmup
278
+ self.det_times.postprocess_time_s.start()
279
+ result = self.postprocess(inputs, result)
280
+ self.det_times.postprocess_time_s.end()
281
+ self.det_times.img_num += 1
282
+
283
+ cm, gm, gu = get_current_memory_mb()
284
+ self.cpu_mem += cm
285
+ self.gpu_mem += gm
286
+ self.gpu_util += gu
287
+ else:
288
+ # preprocess
289
+ self.det_times.preprocess_time_s.start()
290
+ inputs = self.preprocess(batch_image_list)
291
+ self.det_times.preprocess_time_s.end()
292
+
293
+ # model prediction
294
+ self.det_times.inference_time_s.start()
295
+ result = self.predict()
296
+ self.det_times.inference_time_s.end()
297
+
298
+ # postprocess
299
+ self.det_times.postprocess_time_s.start()
300
+ result = self.postprocess(inputs, result)
301
+ self.det_times.postprocess_time_s.end()
302
+ self.det_times.img_num += 1
303
+
304
+ st, ed = 0, result['boxes_num'][0] # start_index, end_index
305
+ for _ind in range(sub_img_num):
306
+ boxes_num = result['boxes_num'][_ind]
307
+ ed = boxes_num
308
+ shift_amount = slice_image_result.starting_pixels[_ind]
309
+ result['boxes'][st:ed][:, 2:4] = result['boxes'][
310
+ st:ed][:, 2:4] + shift_amount
311
+ result['boxes'][st:ed][:, 4:6] = result['boxes'][
312
+ st:ed][:, 4:6] + shift_amount
313
+ merged_bboxs.append(result['boxes'][st:ed])
314
+ st = ed
315
+
316
+ merged_results = {'boxes': []}
317
+ if combine_method == 'nms':
318
+ final_boxes = multiclass_nms(
319
+ np.concatenate(merged_bboxs), num_classes, match_threshold,
320
+ match_metric)
321
+ merged_results['boxes'] = np.concatenate(final_boxes)
322
+ elif combine_method == 'concat':
323
+ merged_results['boxes'] = np.concatenate(merged_bboxs)
324
+ else:
325
+ raise ValueError(
326
+ "Now only support 'nms' or 'concat' to fuse detection results."
327
+ )
328
+ merged_results['boxes_num'] = np.array(
329
+ [len(merged_results['boxes'])], dtype=np.int32)
330
+
331
+ if visual:
332
+ visualize(
333
+ [ori_image], # should be list
334
+ merged_results,
335
+ self.pred_config.labels,
336
+ output_dir=self.output_dir,
337
+ threshold=self.threshold)
338
+
339
+ results.append(merged_results)
340
+ print('Test iter {}'.format(i))
341
+
342
+ results = self.merge_batch_result(results)
343
+ if save_results:
344
+ Path(self.output_dir).mkdir(exist_ok=True)
345
+ self.save_coco_results(
346
+ img_list, results, use_coco_category=FLAGS.use_coco_category)
347
+ return results
348
+
349
+ def predict_image(self,
350
+ image_list,
351
+ run_benchmark=False,
352
+ repeats=1,
353
+ visual=True,
354
+ save_results=False):
355
+ batch_loop_cnt = math.ceil(float(len(image_list)) / self.batch_size)
356
+ results = []
357
+ for i in range(batch_loop_cnt):
358
+ start_index = i * self.batch_size
359
+ end_index = min((i + 1) * self.batch_size, len(image_list))
360
+ batch_image_list = image_list[start_index:end_index]
361
+ if run_benchmark:
362
+ # preprocess
363
+ inputs = self.preprocess(batch_image_list) # warmup
364
+ self.det_times.preprocess_time_s.start()
365
+ inputs = self.preprocess(batch_image_list)
366
+ self.det_times.preprocess_time_s.end()
367
+
368
+ # model prediction
369
+ result = self.predict(repeats=50) # warmup
370
+ self.det_times.inference_time_s.start()
371
+ result = self.predict(repeats=repeats)
372
+ self.det_times.inference_time_s.end(repeats=repeats)
373
+
374
+ # postprocess
375
+ result_warmup = self.postprocess(inputs, result) # warmup
376
+ self.det_times.postprocess_time_s.start()
377
+ result = self.postprocess(inputs, result)
378
+ self.det_times.postprocess_time_s.end()
379
+ self.det_times.img_num += len(batch_image_list)
380
+
381
+ cm, gm, gu = get_current_memory_mb()
382
+ self.cpu_mem += cm
383
+ self.gpu_mem += gm
384
+ self.gpu_util += gu
385
+ else:
386
+ # preprocess
387
+ self.det_times.preprocess_time_s.start()
388
+ inputs = self.preprocess(batch_image_list)
389
+ self.det_times.preprocess_time_s.end()
390
+
391
+ # model prediction
392
+ self.det_times.inference_time_s.start()
393
+ result = self.predict()
394
+ self.det_times.inference_time_s.end()
395
+
396
+ # postprocess
397
+ self.det_times.postprocess_time_s.start()
398
+ result = self.postprocess(inputs, result)
399
+ self.det_times.postprocess_time_s.end()
400
+ self.det_times.img_num += len(batch_image_list)
401
+
402
+ if visual:
403
+ visualize(
404
+ batch_image_list,
405
+ result,
406
+ self.pred_config.labels,
407
+ output_dir=self.output_dir,
408
+ threshold=self.threshold)
409
+ results.append(result)
410
+ print('Test iter {}'.format(i))
411
+ results = self.merge_batch_result(results)
412
+ if save_results:
413
+ Path(self.output_dir).mkdir(exist_ok=True)
414
+ self.save_coco_results(
415
+ image_list, results, use_coco_category=FLAGS.use_coco_category)
416
+ return results
417
+
418
+ def predict_video(self, video_file, camera_id):
419
+ video_out_name = 'output.mp4'
420
+ if camera_id != -1:
421
+ capture = cv2.VideoCapture(camera_id)
422
+ else:
423
+ capture = cv2.VideoCapture(video_file)
424
+ video_out_name = os.path.split(video_file)[-1]
425
+ # Get Video info : resolution, fps, frame count
426
+ width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
427
+ height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
428
+ fps = int(capture.get(cv2.CAP_PROP_FPS))
429
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
430
+ print("fps: %d, frame_count: %d" % (fps, frame_count))
431
+
432
+ if not os.path.exists(self.output_dir):
433
+ os.makedirs(self.output_dir)
434
+ out_path = os.path.join(self.output_dir, video_out_name)
435
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
436
+ writer = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
437
+ index = 1
438
+ while (1):
439
+ ret, frame = capture.read()
440
+ if not ret:
441
+ break
442
+ print('detect frame: %d' % (index))
443
+ index += 1
444
+ results = self.predict_image([frame[:, :, ::-1]], visual=False)
445
+
446
+ im = visualize_box_mask(
447
+ frame,
448
+ results,
449
+ self.pred_config.labels,
450
+ threshold=self.threshold)
451
+ im = np.array(im)
452
+ writer.write(im)
453
+ if camera_id != -1:
454
+ cv2.imshow('Mask Detection', im)
455
+ if cv2.waitKey(1) & 0xFF == ord('q'):
456
+ break
457
+ writer.release()
458
+
459
+ def save_coco_results(self, image_list, results, use_coco_category=False):
460
+ bbox_results = []
461
+ mask_results = []
462
+ idx = 0
463
+ print("Start saving coco json files...")
464
+ for i, box_num in enumerate(results['boxes_num']):
465
+ file_name = os.path.split(image_list[i])[-1]
466
+ if use_coco_category:
467
+ img_id = int(os.path.splitext(file_name)[0])
468
+ else:
469
+ img_id = i
470
+
471
+ if 'boxes' in results:
472
+ boxes = results['boxes'][idx:idx + box_num].tolist()
473
+ bbox_results.extend([{
474
+ 'image_id': img_id,
475
+ 'category_id': coco_clsid2catid[int(box[0])] \
476
+ if use_coco_category else int(box[0]),
477
+ 'file_name': file_name,
478
+ 'bbox': [box[2], box[3], box[4] - box[2],
479
+ box[5] - box[3]], # xyxy -> xywh
480
+ 'score': box[1]} for box in boxes])
481
+
482
+ if 'masks' in results:
483
+ import pycocotools.mask as mask_util
484
+
485
+ boxes = results['boxes'][idx:idx + box_num].tolist()
486
+ masks = results['masks'][i][:box_num].astype(np.uint8)
487
+ seg_res = []
488
+ for box, mask in zip(boxes, masks):
489
+ rle = mask_util.encode(
490
+ np.array(
491
+ mask[:, :, None], dtype=np.uint8, order="F"))[0]
492
+ if 'counts' in rle:
493
+ rle['counts'] = rle['counts'].decode("utf8")
494
+ seg_res.append({
495
+ 'image_id': img_id,
496
+ 'category_id': coco_clsid2catid[int(box[0])] \
497
+ if use_coco_category else int(box[0]),
498
+ 'file_name': file_name,
499
+ 'segmentation': rle,
500
+ 'score': box[1]})
501
+ mask_results.extend(seg_res)
502
+
503
+ idx += box_num
504
+
505
+ if bbox_results:
506
+ bbox_file = os.path.join(self.output_dir, "bbox.json")
507
+ with open(bbox_file, 'w') as f:
508
+ json.dump(bbox_results, f)
509
+ print(f"The bbox result is saved to {bbox_file}")
510
+ if mask_results:
511
+ mask_file = os.path.join(self.output_dir, "mask.json")
512
+ with open(mask_file, 'w') as f:
513
+ json.dump(mask_results, f)
514
+ print(f"The mask result is saved to {mask_file}")
515
+
516
+
517
+ class DetectorSOLOv2(Detector):
518
+ """
519
+ Args:
520
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
521
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
522
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
523
+ batch_size (int): size of pre batch in inference
524
+ trt_min_shape (int): min shape for dynamic shape in trt
525
+ trt_max_shape (int): max shape for dynamic shape in trt
526
+ trt_opt_shape (int): opt shape for dynamic shape in trt
527
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
528
+ calibration, trt_calib_mode need to set True
529
+ cpu_threads (int): cpu threads
530
+ enable_mkldnn (bool): whether to open MKLDNN
531
+ enable_mkldnn_bfloat16 (bool): Whether to turn on mkldnn bfloat16
532
+ output_dir (str): The path of output
533
+ threshold (float): The threshold of score for visualization
534
+
535
+ """
536
+
537
+ def __init__(
538
+ self,
539
+ model_dir,
540
+ device='CPU',
541
+ run_mode='paddle',
542
+ batch_size=1,
543
+ trt_min_shape=1,
544
+ trt_max_shape=1280,
545
+ trt_opt_shape=640,
546
+ trt_calib_mode=False,
547
+ cpu_threads=1,
548
+ enable_mkldnn=False,
549
+ enable_mkldnn_bfloat16=False,
550
+ output_dir='./',
551
+ threshold=0.5, ):
552
+ super(DetectorSOLOv2, self).__init__(
553
+ model_dir=model_dir,
554
+ device=device,
555
+ run_mode=run_mode,
556
+ batch_size=batch_size,
557
+ trt_min_shape=trt_min_shape,
558
+ trt_max_shape=trt_max_shape,
559
+ trt_opt_shape=trt_opt_shape,
560
+ trt_calib_mode=trt_calib_mode,
561
+ cpu_threads=cpu_threads,
562
+ enable_mkldnn=enable_mkldnn,
563
+ enable_mkldnn_bfloat16=enable_mkldnn_bfloat16,
564
+ output_dir=output_dir,
565
+ threshold=threshold, )
566
+
567
+ def predict(self, repeats=1):
568
+ '''
569
+ Args:
570
+ repeats (int): repeat number for prediction
571
+ Returns:
572
+ result (dict): 'segm': np.ndarray,shape:[N, im_h, im_w]
573
+ 'cate_label': label of segm, shape:[N]
574
+ 'cate_score': confidence score of segm, shape:[N]
575
+ '''
576
+ np_label, np_score, np_segms = None, None, None
577
+ for i in range(repeats):
578
+ self.predictor.run()
579
+ output_names = self.predictor.get_output_names()
580
+ np_boxes_num = self.predictor.get_output_handle(output_names[
581
+ 0]).copy_to_cpu()
582
+ np_label = self.predictor.get_output_handle(output_names[
583
+ 1]).copy_to_cpu()
584
+ np_score = self.predictor.get_output_handle(output_names[
585
+ 2]).copy_to_cpu()
586
+ np_segms = self.predictor.get_output_handle(output_names[
587
+ 3]).copy_to_cpu()
588
+
589
+ result = dict(
590
+ segm=np_segms,
591
+ label=np_label,
592
+ score=np_score,
593
+ boxes_num=np_boxes_num)
594
+ return result
595
+
596
+
597
+ class DetectorPicoDet(Detector):
598
+ """
599
+ Args:
600
+ model_dir (str): root path of model.pdiparams, model.pdmodel and infer_cfg.yml
601
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
602
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16)
603
+ batch_size (int): size of pre batch in inference
604
+ trt_min_shape (int): min shape for dynamic shape in trt
605
+ trt_max_shape (int): max shape for dynamic shape in trt
606
+ trt_opt_shape (int): opt shape for dynamic shape in trt
607
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
608
+ calibration, trt_calib_mode need to set True
609
+ cpu_threads (int): cpu threads
610
+ enable_mkldnn (bool): whether to turn on MKLDNN
611
+ enable_mkldnn_bfloat16 (bool): whether to turn on MKLDNN_BFLOAT16
612
+ """
613
+
614
+ def __init__(
615
+ self,
616
+ model_dir,
617
+ device='CPU',
618
+ run_mode='paddle',
619
+ batch_size=1,
620
+ trt_min_shape=1,
621
+ trt_max_shape=1280,
622
+ trt_opt_shape=640,
623
+ trt_calib_mode=False,
624
+ cpu_threads=1,
625
+ enable_mkldnn=False,
626
+ enable_mkldnn_bfloat16=False,
627
+ output_dir='./',
628
+ threshold=0.5, ):
629
+ super(DetectorPicoDet, self).__init__(
630
+ model_dir=model_dir,
631
+ device=device,
632
+ run_mode=run_mode,
633
+ batch_size=batch_size,
634
+ trt_min_shape=trt_min_shape,
635
+ trt_max_shape=trt_max_shape,
636
+ trt_opt_shape=trt_opt_shape,
637
+ trt_calib_mode=trt_calib_mode,
638
+ cpu_threads=cpu_threads,
639
+ enable_mkldnn=enable_mkldnn,
640
+ enable_mkldnn_bfloat16=enable_mkldnn_bfloat16,
641
+ output_dir=output_dir,
642
+ threshold=threshold, )
643
+
644
+ def postprocess(self, inputs, result):
645
+ # postprocess output of predictor
646
+ np_score_list = result['boxes']
647
+ np_boxes_list = result['boxes_num']
648
+ postprocessor = PicoDetPostProcess(
649
+ inputs['image'].shape[2:],
650
+ inputs['im_shape'],
651
+ inputs['scale_factor'],
652
+ strides=self.pred_config.fpn_stride,
653
+ nms_threshold=self.pred_config.nms['nms_threshold'])
654
+ np_boxes, np_boxes_num = postprocessor(np_score_list, np_boxes_list)
655
+ result = dict(boxes=np_boxes, boxes_num=np_boxes_num)
656
+ return result
657
+
658
+ def predict(self, repeats=1):
659
+ '''
660
+ Args:
661
+ repeats (int): repeat number for prediction
662
+ Returns:
663
+ result (dict): include 'boxes': np.ndarray: shape:[N,6], N: number of box,
664
+ matix element:[class, score, x_min, y_min, x_max, y_max]
665
+ '''
666
+ np_score_list, np_boxes_list = [], []
667
+ for i in range(repeats):
668
+ self.predictor.run()
669
+ np_score_list.clear()
670
+ np_boxes_list.clear()
671
+ output_names = self.predictor.get_output_names()
672
+ num_outs = int(len(output_names) / 2)
673
+ for out_idx in range(num_outs):
674
+ np_score_list.append(
675
+ self.predictor.get_output_handle(output_names[out_idx])
676
+ .copy_to_cpu())
677
+ np_boxes_list.append(
678
+ self.predictor.get_output_handle(output_names[
679
+ out_idx + num_outs]).copy_to_cpu())
680
+ result = dict(boxes=np_score_list, boxes_num=np_boxes_list)
681
+ return result
682
+
683
+
684
+ def create_inputs(imgs, im_info):
685
+ """generate input for different model type
686
+ Args:
687
+ imgs (list(numpy)): list of images (np.ndarray)
688
+ im_info (list(dict)): list of image info
689
+ Returns:
690
+ inputs (dict): input of model
691
+ """
692
+ inputs = {}
693
+
694
+ im_shape = []
695
+ scale_factor = []
696
+ if len(imgs) == 1:
697
+ inputs['image'] = np.array((imgs[0], )).astype('float32')
698
+ inputs['im_shape'] = np.array(
699
+ (im_info[0]['im_shape'], )).astype('float32')
700
+ inputs['scale_factor'] = np.array(
701
+ (im_info[0]['scale_factor'], )).astype('float32')
702
+ return inputs
703
+
704
+ for e in im_info:
705
+ im_shape.append(np.array((e['im_shape'], )).astype('float32'))
706
+ scale_factor.append(np.array((e['scale_factor'], )).astype('float32'))
707
+
708
+ inputs['im_shape'] = np.concatenate(im_shape, axis=0)
709
+ inputs['scale_factor'] = np.concatenate(scale_factor, axis=0)
710
+
711
+ imgs_shape = [[e.shape[1], e.shape[2]] for e in imgs]
712
+ max_shape_h = max([e[0] for e in imgs_shape])
713
+ max_shape_w = max([e[1] for e in imgs_shape])
714
+ padding_imgs = []
715
+ for img in imgs:
716
+ im_c, im_h, im_w = img.shape[:]
717
+ padding_im = np.zeros(
718
+ (im_c, max_shape_h, max_shape_w), dtype=np.float32)
719
+ padding_im[:, :im_h, :im_w] = img
720
+ padding_imgs.append(padding_im)
721
+ inputs['image'] = np.stack(padding_imgs, axis=0)
722
+ return inputs
723
+
724
+
725
+ class PredictConfig():
726
+ """set config of preprocess, postprocess and visualize
727
+ Args:
728
+ model_dir (str): root path of model.yml
729
+ """
730
+
731
+ def __init__(self, model_dir):
732
+ # parsing Yaml config for Preprocess
733
+ deploy_file = os.path.join(model_dir, 'infer_cfg.yml')
734
+ with open(deploy_file) as f:
735
+ yml_conf = yaml.safe_load(f)
736
+ self.check_model(yml_conf)
737
+ self.arch = yml_conf['arch']
738
+ self.preprocess_infos = yml_conf['Preprocess']
739
+ self.min_subgraph_size = yml_conf['min_subgraph_size']
740
+ self.labels = yml_conf['label_list']
741
+ self.mask = False
742
+ self.use_dynamic_shape = yml_conf['use_dynamic_shape']
743
+ if 'mask' in yml_conf:
744
+ self.mask = yml_conf['mask']
745
+ self.tracker = None
746
+ if 'tracker' in yml_conf:
747
+ self.tracker = yml_conf['tracker']
748
+ if 'NMS' in yml_conf:
749
+ self.nms = yml_conf['NMS']
750
+ if 'fpn_stride' in yml_conf:
751
+ self.fpn_stride = yml_conf['fpn_stride']
752
+ if self.arch == 'RCNN' and yml_conf.get('export_onnx', False):
753
+ print(
754
+ 'The RCNN export model is used for ONNX and it only supports batch_size = 1'
755
+ )
756
+ self.print_config()
757
+
758
+ def check_model(self, yml_conf):
759
+ """
760
+ Raises:
761
+ ValueError: loaded model not in supported model type
762
+ """
763
+ for support_model in SUPPORT_MODELS:
764
+ if support_model in yml_conf['arch']:
765
+ return True
766
+ raise ValueError("Unsupported arch: {}, expect {}".format(yml_conf[
767
+ 'arch'], SUPPORT_MODELS))
768
+
769
+ def print_config(self):
770
+ print('----------- Model Configuration -----------')
771
+ print('%s: %s' % ('Model Arch', self.arch))
772
+ print('%s: ' % ('Transform Order'))
773
+ for op_info in self.preprocess_infos:
774
+ print('--%s: %s' % ('transform op', op_info['type']))
775
+ print('--------------------------------------------')
776
+
777
+
778
+ def load_predictor(model_dir,
779
+ run_mode='paddle',
780
+ batch_size=1,
781
+ device='CPU',
782
+ min_subgraph_size=3,
783
+ use_dynamic_shape=False,
784
+ trt_min_shape=1,
785
+ trt_max_shape=1280,
786
+ trt_opt_shape=640,
787
+ trt_calib_mode=False,
788
+ cpu_threads=1,
789
+ enable_mkldnn=False,
790
+ enable_mkldnn_bfloat16=False,
791
+ delete_shuffle_pass=False):
792
+ """set AnalysisConfig, generate AnalysisPredictor
793
+ Args:
794
+ model_dir (str): root path of __model__ and __params__
795
+ device (str): Choose the device you want to run, it can be: CPU/GPU/XPU, default is CPU
796
+ run_mode (str): mode of running(paddle/trt_fp32/trt_fp16/trt_int8)
797
+ use_dynamic_shape (bool): use dynamic shape or not
798
+ trt_min_shape (int): min shape for dynamic shape in trt
799
+ trt_max_shape (int): max shape for dynamic shape in trt
800
+ trt_opt_shape (int): opt shape for dynamic shape in trt
801
+ trt_calib_mode (bool): If the model is produced by TRT offline quantitative
802
+ calibration, trt_calib_mode need to set True
803
+ delete_shuffle_pass (bool): whether to remove shuffle_channel_detect_pass in TensorRT.
804
+ Used by action model.
805
+ Returns:
806
+ predictor (PaddlePredictor): AnalysisPredictor
807
+ Raises:
808
+ ValueError: predict by TensorRT need device == 'GPU'.
809
+ """
810
+ if device != 'GPU' and run_mode != 'paddle':
811
+ raise ValueError(
812
+ "Predict by TensorRT mode: {}, expect device=='GPU', but device == {}"
813
+ .format(run_mode, device))
814
+ infer_model = os.path.join(model_dir, 'model.pdmodel')
815
+ infer_params = os.path.join(model_dir, 'model.pdiparams')
816
+ if not os.path.exists(infer_model):
817
+ infer_model = os.path.join(model_dir, 'inference.pdmodel')
818
+ infer_params = os.path.join(model_dir, 'inference.pdiparams')
819
+ if not os.path.exists(infer_model):
820
+ raise ValueError("Cannot find any inference model in dir: {},".
821
+ format(model_dir))
822
+ config = Config(infer_model, infer_params)
823
+ if device == 'GPU':
824
+ # initial GPU memory(M), device ID
825
+ config.enable_use_gpu(200, 0)
826
+ # optimize graph and fuse op
827
+ config.switch_ir_optim(True)
828
+ elif device == 'XPU':
829
+ config.enable_lite_engine()
830
+ config.enable_xpu(10 * 1024 * 1024)
831
+ else:
832
+ config.disable_gpu()
833
+ config.set_cpu_math_library_num_threads(cpu_threads)
834
+ if enable_mkldnn:
835
+ try:
836
+ # cache 10 different shapes for mkldnn to avoid memory leak
837
+ config.set_mkldnn_cache_capacity(10)
838
+ config.enable_mkldnn()
839
+ if enable_mkldnn_bfloat16:
840
+ config.enable_mkldnn_bfloat16()
841
+ except Exception as e:
842
+ print(
843
+ "The current environment does not support `mkldnn`, so disable mkldnn."
844
+ )
845
+ pass
846
+
847
+ precision_map = {
848
+ 'trt_int8': Config.Precision.Int8,
849
+ 'trt_fp32': Config.Precision.Float32,
850
+ 'trt_fp16': Config.Precision.Half
851
+ }
852
+ if run_mode in precision_map.keys():
853
+ config.enable_tensorrt_engine(
854
+ workspace_size=(1 << 25) * batch_size,
855
+ max_batch_size=batch_size,
856
+ min_subgraph_size=min_subgraph_size,
857
+ precision_mode=precision_map[run_mode],
858
+ use_static=False,
859
+ use_calib_mode=trt_calib_mode)
860
+
861
+ if use_dynamic_shape:
862
+ min_input_shape = {
863
+ 'image': [batch_size, 3, trt_min_shape, trt_min_shape]
864
+ }
865
+ max_input_shape = {
866
+ 'image': [batch_size, 3, trt_max_shape, trt_max_shape]
867
+ }
868
+ opt_input_shape = {
869
+ 'image': [batch_size, 3, trt_opt_shape, trt_opt_shape]
870
+ }
871
+ config.set_trt_dynamic_shape_info(min_input_shape, max_input_shape,
872
+ opt_input_shape)
873
+ print('trt set dynamic shape done!')
874
+
875
+ # disable print log when predict
876
+ config.disable_glog_info()
877
+ # enable shared memory
878
+ config.enable_memory_optim()
879
+ # disable feed, fetch OP, needed by zero_copy_run
880
+ config.switch_use_feed_fetch_ops(False)
881
+ if delete_shuffle_pass:
882
+ config.delete_pass("shuffle_channel_detect_pass")
883
+ predictor = create_predictor(config)
884
+ return predictor, config
885
+
886
+
887
+ def get_test_images(infer_dir, infer_img):
888
+ """
889
+ Get image path list in TEST mode
890
+ """
891
+ assert infer_img is not None or infer_dir is not None, \
892
+ "--image_file or --image_dir should be set"
893
+ assert infer_img is None or os.path.isfile(infer_img), \
894
+ "{} is not a file".format(infer_img)
895
+ assert infer_dir is None or os.path.isdir(infer_dir), \
896
+ "{} is not a directory".format(infer_dir)
897
+
898
+ # infer_img has a higher priority
899
+ if infer_img and os.path.isfile(infer_img):
900
+ return [infer_img]
901
+
902
+ images = set()
903
+ infer_dir = os.path.abspath(infer_dir)
904
+ assert os.path.isdir(infer_dir), \
905
+ "infer_dir {} is not a directory".format(infer_dir)
906
+ exts = ['jpg', 'jpeg', 'png', 'bmp']
907
+ exts += [ext.upper() for ext in exts]
908
+ for ext in exts:
909
+ images.update(glob.glob('{}/*.{}'.format(infer_dir, ext)))
910
+ images = list(images)
911
+
912
+ assert len(images) > 0, "no image found in {}".format(infer_dir)
913
+ print("Found {} inference images in total.".format(len(images)))
914
+
915
+ return images
916
+
917
+
918
+ def visualize(image_list, result, labels, output_dir='output/', threshold=0.5):
919
+ # visualize the predict result
920
+ start_idx = 0
921
+ for idx, image_file in enumerate(image_list):
922
+ im_bboxes_num = result['boxes_num'][idx]
923
+ im_results = {}
924
+ if 'boxes' in result:
925
+ im_results['boxes'] = result['boxes'][start_idx:start_idx +
926
+ im_bboxes_num, :]
927
+ if 'masks' in result:
928
+ im_results['masks'] = result['masks'][start_idx:start_idx +
929
+ im_bboxes_num, :]
930
+ if 'segm' in result:
931
+ im_results['segm'] = result['segm'][start_idx:start_idx +
932
+ im_bboxes_num, :]
933
+ if 'label' in result:
934
+ im_results['label'] = result['label'][start_idx:start_idx +
935
+ im_bboxes_num]
936
+ if 'score' in result:
937
+ im_results['score'] = result['score'][start_idx:start_idx +
938
+ im_bboxes_num]
939
+
940
+ start_idx += im_bboxes_num
941
+ im = visualize_box_mask(
942
+ image_file, im_results, labels, threshold=threshold)
943
+ img_name = os.path.split(image_file)[-1]
944
+ if not os.path.exists(output_dir):
945
+ os.makedirs(output_dir)
946
+ out_path = os.path.join(output_dir, img_name)
947
+ im.save(out_path, quality=95)
948
+ print("save result to: " + out_path)
949
+
950
+
951
+ def print_arguments(args):
952
+ print('----------- Running Arguments -----------')
953
+ for arg, value in sorted(vars(args).items()):
954
+ print('%s: %s' % (arg, value))
955
+ print('------------------------------------------')
956
+
957
+
958
+ def main():
959
+ deploy_file = os.path.join(FLAGS.model_dir, 'infer_cfg.yml')
960
+ with open(deploy_file) as f:
961
+ yml_conf = yaml.safe_load(f)
962
+ arch = yml_conf['arch']
963
+ detector_func = 'Detector'
964
+ if arch == 'SOLOv2':
965
+ detector_func = 'DetectorSOLOv2'
966
+ elif arch == 'PicoDet':
967
+ detector_func = 'DetectorPicoDet'
968
+
969
+ detector = eval(detector_func)(
970
+ FLAGS.model_dir,
971
+ device=FLAGS.device,
972
+ run_mode=FLAGS.run_mode,
973
+ batch_size=FLAGS.batch_size,
974
+ trt_min_shape=FLAGS.trt_min_shape,
975
+ trt_max_shape=FLAGS.trt_max_shape,
976
+ trt_opt_shape=FLAGS.trt_opt_shape,
977
+ trt_calib_mode=FLAGS.trt_calib_mode,
978
+ cpu_threads=FLAGS.cpu_threads,
979
+ enable_mkldnn=FLAGS.enable_mkldnn,
980
+ enable_mkldnn_bfloat16=FLAGS.enable_mkldnn_bfloat16,
981
+ threshold=FLAGS.threshold,
982
+ output_dir=FLAGS.output_dir)
983
+
984
+ # predict from video file or camera video stream
985
+ if FLAGS.video_file is not None or FLAGS.camera_id != -1:
986
+ detector.predict_video(FLAGS.video_file, FLAGS.camera_id)
987
+ else:
988
+ # predict from image
989
+ if FLAGS.image_dir is None and FLAGS.image_file is not None:
990
+ assert FLAGS.batch_size == 1, "batch_size should be 1, when image_file is not None"
991
+ img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
992
+ if FLAGS.slice_infer:
993
+ detector.predict_image_slice(
994
+ img_list,
995
+ FLAGS.slice_size,
996
+ FLAGS.overlap_ratio,
997
+ FLAGS.combine_method,
998
+ FLAGS.match_threshold,
999
+ FLAGS.match_metric,
1000
+ visual=FLAGS.save_images,
1001
+ save_results=FLAGS.save_results)
1002
+ else:
1003
+ detector.predict_image(
1004
+ img_list,
1005
+ FLAGS.run_benchmark,
1006
+ repeats=100,
1007
+ visual=FLAGS.save_images,
1008
+ save_results=FLAGS.save_results)
1009
+ if not FLAGS.run_benchmark:
1010
+ detector.det_times.info(average=True)
1011
+ else:
1012
+ mode = FLAGS.run_mode
1013
+ model_dir = FLAGS.model_dir
1014
+ model_info = {
1015
+ 'model_name': model_dir.strip('/').split('/')[-1],
1016
+ 'precision': mode.split('_')[-1]
1017
+ }
1018
+ bench_log(detector, img_list, model_info, name='DET')
1019
+
1020
+
1021
+ if __name__ == '__main__':
1022
+ paddle.enable_static()
1023
+ parser = argsparser()
1024
+ FLAGS = parser.parse_args()
1025
+ print_arguments(FLAGS)
1026
+ FLAGS.device = FLAGS.device.upper()
1027
+ assert FLAGS.device in ['CPU', 'GPU', 'XPU'
1028
+ ], "device should be CPU, GPU or XPU"
1029
+ assert not FLAGS.use_gpu, "use_gpu has been deprecated, please use --device"
1030
+
1031
+ assert not (
1032
+ FLAGS.enable_mkldnn == False and FLAGS.enable_mkldnn_bfloat16 == True
1033
+ ), 'To enable mkldnn bfloat, please turn on both enable_mkldnn and enable_mkldnn_bfloat16'
1034
+
1035
+ main()