更换文档检测模型
This commit is contained in:
30
paddle_detection/ppdet/modeling/mot/tracker/__init__.py
Normal file
30
paddle_detection/ppdet/modeling/mot/tracker/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import base_jde_tracker
|
||||
from . import base_sde_tracker
|
||||
|
||||
from .base_jde_tracker import *
|
||||
from .base_sde_tracker import *
|
||||
|
||||
from . import jde_tracker
|
||||
from . import deepsort_tracker
|
||||
from . import ocsort_tracker
|
||||
from . import center_tracker
|
||||
|
||||
from .jde_tracker import *
|
||||
from .deepsort_tracker import *
|
||||
from .ocsort_tracker import *
|
||||
from .botsort_tracker import *
|
||||
from .center_tracker import *
|
||||
311
paddle_detection/ppdet/modeling/mot/tracker/base_jde_tracker.py
Normal file
311
paddle_detection/ppdet/modeling/mot/tracker/base_jde_tracker.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/Zhongdao/Towards-Realtime-MOT/blob/master/tracker/multitracker.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
from collections import deque, OrderedDict
|
||||
from ..matching import jde_matching as matching
|
||||
from ppdet.core.workspace import register, serializable
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
__all__ = [
|
||||
'TrackState',
|
||||
'BaseTrack',
|
||||
'STrack',
|
||||
'joint_stracks',
|
||||
'sub_stracks',
|
||||
'remove_duplicate_stracks',
|
||||
]
|
||||
|
||||
|
||||
class TrackState(object):
|
||||
New = 0
|
||||
Tracked = 1
|
||||
Lost = 2
|
||||
Removed = 3
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class BaseTrack(object):
|
||||
_count_dict = defaultdict(int) # support single class and multi classes
|
||||
|
||||
track_id = 0
|
||||
is_activated = False
|
||||
state = TrackState.New
|
||||
|
||||
history = OrderedDict()
|
||||
features = []
|
||||
curr_feat = None
|
||||
score = 0
|
||||
start_frame = 0
|
||||
frame_id = 0
|
||||
time_since_update = 0
|
||||
|
||||
# multi-camera
|
||||
location = (np.inf, np.inf)
|
||||
|
||||
@property
|
||||
def end_frame(self):
|
||||
return self.frame_id
|
||||
|
||||
@staticmethod
|
||||
def next_id(cls_id):
|
||||
BaseTrack._count_dict[cls_id] += 1
|
||||
return BaseTrack._count_dict[cls_id]
|
||||
|
||||
# @even: reset track id
|
||||
@staticmethod
|
||||
def init_count(num_classes):
|
||||
"""
|
||||
Initiate _count for all object classes
|
||||
:param num_classes:
|
||||
"""
|
||||
for cls_id in range(num_classes):
|
||||
BaseTrack._count_dict[cls_id] = 0
|
||||
|
||||
@staticmethod
|
||||
def reset_track_count(cls_id):
|
||||
BaseTrack._count_dict[cls_id] = 0
|
||||
|
||||
def activate(self, *args):
|
||||
raise NotImplementedError
|
||||
|
||||
def predict(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def mark_lost(self):
|
||||
self.state = TrackState.Lost
|
||||
|
||||
def mark_removed(self):
|
||||
self.state = TrackState.Removed
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class STrack(BaseTrack):
|
||||
def __init__(self, tlwh, score, cls_id, buff_size=30, temp_feat=None):
|
||||
# wait activate
|
||||
self._tlwh = np.asarray(tlwh, dtype=np.float32)
|
||||
self.score = score
|
||||
self.cls_id = cls_id
|
||||
self.track_len = 0
|
||||
|
||||
self.kalman_filter = None
|
||||
self.mean, self.covariance = None, None
|
||||
self.is_activated = False
|
||||
|
||||
self.use_reid = True if temp_feat is not None else False
|
||||
if self.use_reid:
|
||||
self.smooth_feat = None
|
||||
self.update_features(temp_feat)
|
||||
self.features = deque([], maxlen=buff_size)
|
||||
self.alpha = 0.9
|
||||
|
||||
def update_features(self, feat):
|
||||
# L2 normalizing, this function has no use for BYTETracker
|
||||
feat /= np.linalg.norm(feat)
|
||||
self.curr_feat = feat
|
||||
if self.smooth_feat is None:
|
||||
self.smooth_feat = feat
|
||||
else:
|
||||
self.smooth_feat = self.alpha * self.smooth_feat + (1.0 - self.alpha
|
||||
) * feat
|
||||
self.features.append(feat)
|
||||
self.smooth_feat /= np.linalg.norm(self.smooth_feat)
|
||||
|
||||
def predict(self):
|
||||
mean_state = self.mean.copy()
|
||||
if self.state != TrackState.Tracked:
|
||||
mean_state[7] = 0
|
||||
self.mean, self.covariance = self.kalman_filter.predict(mean_state,
|
||||
self.covariance)
|
||||
|
||||
@staticmethod
|
||||
def multi_predict(tracks, kalman_filter):
|
||||
if len(tracks) > 0:
|
||||
multi_mean = np.asarray([track.mean.copy() for track in tracks])
|
||||
multi_covariance = np.asarray(
|
||||
[track.covariance for track in tracks])
|
||||
for i, st in enumerate(tracks):
|
||||
if st.state != TrackState.Tracked:
|
||||
multi_mean[i][7] = 0
|
||||
multi_mean, multi_covariance = kalman_filter.multi_predict(
|
||||
multi_mean, multi_covariance)
|
||||
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
|
||||
tracks[i].mean = mean
|
||||
tracks[i].covariance = cov
|
||||
|
||||
@staticmethod
|
||||
def multi_gmc(stracks, H=np.eye(2, 3)):
|
||||
if len(stracks) > 0:
|
||||
multi_mean = np.asarray([st.mean.copy() for st in stracks])
|
||||
multi_covariance = np.asarray([st.covariance for st in stracks])
|
||||
|
||||
R = H[:2, :2]
|
||||
R8x8 = np.kron(np.eye(4, dtype=float), R)
|
||||
t = H[:2, 2]
|
||||
|
||||
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
|
||||
mean = R8x8.dot(mean)
|
||||
mean[:2] += t
|
||||
cov = R8x8.dot(cov).dot(R8x8.transpose())
|
||||
|
||||
stracks[i].mean = mean
|
||||
stracks[i].covariance = cov
|
||||
|
||||
def reset_track_id(self):
|
||||
self.reset_track_count(self.cls_id)
|
||||
|
||||
def activate(self, kalman_filter, frame_id):
|
||||
"""Start a new track"""
|
||||
self.kalman_filter = kalman_filter
|
||||
# update track id for the object class
|
||||
self.track_id = self.next_id(self.cls_id)
|
||||
self.mean, self.covariance = self.kalman_filter.initiate(
|
||||
self.tlwh_to_xyah(self._tlwh))
|
||||
|
||||
self.track_len = 0
|
||||
self.state = TrackState.Tracked # set flag 'tracked'
|
||||
|
||||
if frame_id == 1: # to record the first frame's detection result
|
||||
self.is_activated = True
|
||||
|
||||
self.frame_id = frame_id
|
||||
self.start_frame = frame_id
|
||||
|
||||
def re_activate(self, new_track, frame_id, new_id=False):
|
||||
self.mean, self.covariance = self.kalman_filter.update(
|
||||
self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh))
|
||||
if self.use_reid:
|
||||
self.update_features(new_track.curr_feat)
|
||||
self.track_len = 0
|
||||
self.state = TrackState.Tracked
|
||||
self.is_activated = True
|
||||
self.frame_id = frame_id
|
||||
if new_id: # update track id for the object class
|
||||
self.track_id = self.next_id(self.cls_id)
|
||||
|
||||
def update(self, new_track, frame_id, update_feature=True):
|
||||
self.frame_id = frame_id
|
||||
self.track_len += 1
|
||||
|
||||
new_tlwh = new_track.tlwh
|
||||
self.mean, self.covariance = self.kalman_filter.update(
|
||||
self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh))
|
||||
self.state = TrackState.Tracked # set flag 'tracked'
|
||||
self.is_activated = True # set flag 'activated'
|
||||
|
||||
self.score = new_track.score
|
||||
if update_feature and self.use_reid:
|
||||
self.update_features(new_track.curr_feat)
|
||||
|
||||
@property
|
||||
def tlwh(self):
|
||||
"""Get current position in bounding box format `(top left x, top left y,
|
||||
width, height)`.
|
||||
"""
|
||||
if self.mean is None:
|
||||
return self._tlwh.copy()
|
||||
|
||||
ret = self.mean[:4].copy()
|
||||
ret[2] *= ret[3]
|
||||
ret[:2] -= ret[2:] / 2
|
||||
return ret
|
||||
|
||||
@property
|
||||
def tlbr(self):
|
||||
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
|
||||
`(top left, bottom right)`.
|
||||
"""
|
||||
ret = self.tlwh.copy()
|
||||
ret[2:] += ret[:2]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def tlwh_to_xyah(tlwh):
|
||||
"""Convert bounding box to format `(center x, center y, aspect ratio,
|
||||
height)`, where the aspect ratio is `width / height`.
|
||||
"""
|
||||
ret = np.asarray(tlwh).copy()
|
||||
ret[:2] += ret[2:] / 2
|
||||
ret[2] /= ret[3]
|
||||
return ret
|
||||
|
||||
def to_xyah(self):
|
||||
return self.tlwh_to_xyah(self.tlwh)
|
||||
|
||||
@staticmethod
|
||||
def tlbr_to_tlwh(tlbr):
|
||||
ret = np.asarray(tlbr).copy()
|
||||
ret[2:] -= ret[:2]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def tlwh_to_tlbr(tlwh):
|
||||
ret = np.asarray(tlwh).copy()
|
||||
ret[2:] += ret[:2]
|
||||
return ret
|
||||
|
||||
def __repr__(self):
|
||||
return 'OT_({}-{})_({}-{})'.format(self.cls_id, self.track_id,
|
||||
self.start_frame, self.end_frame)
|
||||
|
||||
|
||||
def joint_stracks(tlista, tlistb):
|
||||
exists = {}
|
||||
res = []
|
||||
for t in tlista:
|
||||
exists[t.track_id] = 1
|
||||
res.append(t)
|
||||
for t in tlistb:
|
||||
tid = t.track_id
|
||||
if not exists.get(tid, 0):
|
||||
exists[tid] = 1
|
||||
res.append(t)
|
||||
return res
|
||||
|
||||
|
||||
def sub_stracks(tlista, tlistb):
|
||||
stracks = {}
|
||||
for t in tlista:
|
||||
stracks[t.track_id] = t
|
||||
for t in tlistb:
|
||||
tid = t.track_id
|
||||
if stracks.get(tid, 0):
|
||||
del stracks[tid]
|
||||
return list(stracks.values())
|
||||
|
||||
|
||||
def remove_duplicate_stracks(stracksa, stracksb):
|
||||
pdist = matching.iou_distance(stracksa, stracksb)
|
||||
pairs = np.where(pdist < 0.15)
|
||||
dupa, dupb = list(), list()
|
||||
for p, q in zip(*pairs):
|
||||
timep = stracksa[p].frame_id - stracksa[p].start_frame
|
||||
timeq = stracksb[q].frame_id - stracksb[q].start_frame
|
||||
if timep > timeq:
|
||||
dupb.append(q)
|
||||
else:
|
||||
dupa.append(p)
|
||||
resa = [t for i, t in enumerate(stracksa) if not i in dupa]
|
||||
resb = [t for i, t in enumerate(stracksb) if not i in dupb]
|
||||
return resa, resb
|
||||
156
paddle_detection/ppdet/modeling/mot/tracker/base_sde_tracker.py
Normal file
156
paddle_detection/ppdet/modeling/mot/tracker/base_sde_tracker.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/nwojke/deep_sort/blob/master/deep_sort/track.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from ppdet.core.workspace import register, serializable
|
||||
|
||||
__all__ = ['TrackState', 'Track']
|
||||
|
||||
|
||||
class TrackState(object):
|
||||
"""
|
||||
Enumeration type for the single target track state. Newly created tracks are
|
||||
classified as `tentative` until enough evidence has been collected. Then,
|
||||
the track state is changed to `confirmed`. Tracks that are no longer alive
|
||||
are classified as `deleted` to mark them for removal from the set of active
|
||||
tracks.
|
||||
"""
|
||||
Tentative = 1
|
||||
Confirmed = 2
|
||||
Deleted = 3
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class Track(object):
|
||||
"""
|
||||
A single target track with state space `(x, y, a, h)` and associated
|
||||
velocities, where `(x, y)` is the center of the bounding box, `a` is the
|
||||
aspect ratio and `h` is the height.
|
||||
|
||||
Args:
|
||||
mean (ndarray): Mean vector of the initial state distribution.
|
||||
covariance (ndarray): Covariance matrix of the initial state distribution.
|
||||
track_id (int): A unique track identifier.
|
||||
n_init (int): Number of consecutive detections before the track is confirmed.
|
||||
The track state is set to `Deleted` if a miss occurs within the first
|
||||
`n_init` frames.
|
||||
max_age (int): The maximum number of consecutive misses before the track
|
||||
state is set to `Deleted`.
|
||||
cls_id (int): The category id of the tracked box.
|
||||
score (float): The confidence score of the tracked box.
|
||||
feature (Optional[ndarray]): Feature vector of the detection this track
|
||||
originates from. If not None, this feature is added to the `features` cache.
|
||||
|
||||
Attributes:
|
||||
hits (int): Total number of measurement updates.
|
||||
age (int): Total number of frames since first occurance.
|
||||
time_since_update (int): Total number of frames since last measurement
|
||||
update.
|
||||
state (TrackState): The current track state.
|
||||
features (List[ndarray]): A cache of features. On each measurement update,
|
||||
the associated feature vector is added to this list.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
mean,
|
||||
covariance,
|
||||
track_id,
|
||||
n_init,
|
||||
max_age,
|
||||
cls_id,
|
||||
score,
|
||||
feature=None):
|
||||
self.mean = mean
|
||||
self.covariance = covariance
|
||||
self.track_id = track_id
|
||||
self.hits = 1
|
||||
self.age = 1
|
||||
self.time_since_update = 0
|
||||
self.cls_id = cls_id
|
||||
self.score = score
|
||||
self.start_time = datetime.datetime.now()
|
||||
|
||||
self.state = TrackState.Tentative
|
||||
self.features = []
|
||||
self.feat = feature
|
||||
if feature is not None:
|
||||
self.features.append(feature)
|
||||
|
||||
self._n_init = n_init
|
||||
self._max_age = max_age
|
||||
|
||||
def to_tlwh(self):
|
||||
"""Get position in format `(top left x, top left y, width, height)`."""
|
||||
ret = self.mean[:4].copy()
|
||||
ret[2] *= ret[3]
|
||||
ret[:2] -= ret[2:] / 2
|
||||
return ret
|
||||
|
||||
def to_tlbr(self):
|
||||
"""Get position in bounding box format `(min x, miny, max x, max y)`."""
|
||||
ret = self.to_tlwh()
|
||||
ret[2:] = ret[:2] + ret[2:]
|
||||
return ret
|
||||
|
||||
def predict(self, kalman_filter):
|
||||
"""
|
||||
Propagate the state distribution to the current time step using a Kalman
|
||||
filter prediction step.
|
||||
"""
|
||||
self.mean, self.covariance = kalman_filter.predict(self.mean,
|
||||
self.covariance)
|
||||
self.age += 1
|
||||
self.time_since_update += 1
|
||||
|
||||
def update(self, kalman_filter, detection):
|
||||
"""
|
||||
Perform Kalman filter measurement update step and update the associated
|
||||
detection feature cache.
|
||||
"""
|
||||
self.mean, self.covariance = kalman_filter.update(self.mean,
|
||||
self.covariance,
|
||||
detection.to_xyah())
|
||||
self.features.append(detection.feature)
|
||||
self.feat = detection.feature
|
||||
self.cls_id = detection.cls_id
|
||||
self.score = detection.score
|
||||
|
||||
self.hits += 1
|
||||
self.time_since_update = 0
|
||||
if self.state == TrackState.Tentative and self.hits >= self._n_init:
|
||||
self.state = TrackState.Confirmed
|
||||
|
||||
def mark_missed(self):
|
||||
"""Mark this track as missed (no association at the current time step).
|
||||
"""
|
||||
if self.state == TrackState.Tentative:
|
||||
self.state = TrackState.Deleted
|
||||
elif self.time_since_update > self._max_age:
|
||||
self.state = TrackState.Deleted
|
||||
|
||||
def is_tentative(self):
|
||||
"""Returns True if this track is tentative (unconfirmed)."""
|
||||
return self.state == TrackState.Tentative
|
||||
|
||||
def is_confirmed(self):
|
||||
"""Returns True if this track is confirmed."""
|
||||
return self.state == TrackState.Confirmed
|
||||
|
||||
def is_deleted(self):
|
||||
"""Returns True if this track is dead and should be deleted."""
|
||||
return self.state == TrackState.Deleted
|
||||
242
paddle_detection/ppdet/modeling/mot/tracker/botsort_tracker.py
Normal file
242
paddle_detection/ppdet/modeling/mot/tracker/botsort_tracker.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/WWangYuHsiang/SMILEtrack/blob/main/BoT-SORT/tracker/bot_sort.py
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from ..matching import jde_matching as matching
|
||||
from ..motion import GMC
|
||||
from .base_jde_tracker import TrackState, STrack
|
||||
from .base_jde_tracker import joint_stracks, sub_stracks, remove_duplicate_stracks
|
||||
from ..motion import KalmanFilter
|
||||
|
||||
from ppdet.core.workspace import register, serializable
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class BOTSORTTracker(object):
|
||||
"""
|
||||
BOTSORT tracker, support single class
|
||||
|
||||
Args:
|
||||
track_high_thresh (float): threshold of detection high score
|
||||
track_low_thresh (float): threshold of remove detection score
|
||||
new_track_thresh (float): threshold of new track score
|
||||
match_thresh (float): iou threshold for associate
|
||||
track_buffer (int): tracking reserved frames,default 30
|
||||
min_box_area (float): reserved min box
|
||||
camera_motion (bool): Whether use camera motion, default False
|
||||
cmc_method (str): camera motion method,defalut sparseOptFlow
|
||||
frame_rate (int): fps buffer_size=int(frame_rate / 30.0 * track_buffer)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
track_high_thresh=0.3,
|
||||
track_low_thresh=0.2,
|
||||
new_track_thresh=0.4,
|
||||
match_thresh=0.7,
|
||||
track_buffer=30,
|
||||
min_box_area=0,
|
||||
camera_motion=False,
|
||||
cmc_method='sparseOptFlow',
|
||||
frame_rate=30):
|
||||
|
||||
self.tracked_stracks = [] # type: list[STrack]
|
||||
self.lost_stracks = [] # type: list[STrack]
|
||||
self.removed_stracks = [] # type: list[STrack]
|
||||
|
||||
self.frame_id = 0
|
||||
|
||||
self.track_high_thresh = track_high_thresh
|
||||
self.track_low_thresh = track_low_thresh
|
||||
self.new_track_thresh = new_track_thresh
|
||||
self.match_thresh = match_thresh
|
||||
self.buffer_size = int(frame_rate / 30.0 * track_buffer)
|
||||
self.max_time_lost = self.buffer_size
|
||||
self.kalman_filter = KalmanFilter()
|
||||
self.min_box_area = min_box_area
|
||||
|
||||
self.camera_motion = camera_motion
|
||||
self.gmc = GMC(method=cmc_method)
|
||||
|
||||
def update(self, output_results, img=None):
|
||||
self.frame_id += 1
|
||||
activated_starcks = []
|
||||
refind_stracks = []
|
||||
lost_stracks = []
|
||||
removed_stracks = []
|
||||
|
||||
if len(output_results):
|
||||
bboxes = output_results[:, 2:6]
|
||||
scores = output_results[:, 1]
|
||||
classes = output_results[:, 0]
|
||||
|
||||
# Remove bad detections
|
||||
lowest_inds = scores > self.track_low_thresh
|
||||
bboxes = bboxes[lowest_inds]
|
||||
scores = scores[lowest_inds]
|
||||
classes = classes[lowest_inds]
|
||||
|
||||
# Find high threshold detections
|
||||
remain_inds = scores > self.track_high_thresh
|
||||
dets = bboxes[remain_inds]
|
||||
scores_keep = scores[remain_inds]
|
||||
classes_keep = classes[remain_inds]
|
||||
|
||||
else:
|
||||
bboxes = []
|
||||
scores = []
|
||||
classes = []
|
||||
dets = []
|
||||
scores_keep = []
|
||||
classes_keep = []
|
||||
|
||||
if len(dets) > 0:
|
||||
'''Detections'''
|
||||
detections = [
|
||||
STrack(STrack.tlbr_to_tlwh(tlbr), s, c)
|
||||
for (tlbr, s, c) in zip(dets, scores_keep, classes_keep)
|
||||
]
|
||||
else:
|
||||
detections = []
|
||||
''' Add newly detected tracklets to tracked_stracks'''
|
||||
unconfirmed = []
|
||||
tracked_stracks = [] # type: list[STrack]
|
||||
for track in self.tracked_stracks:
|
||||
if not track.is_activated:
|
||||
unconfirmed.append(track)
|
||||
else:
|
||||
tracked_stracks.append(track)
|
||||
''' Step 2: First association, with high score detection boxes'''
|
||||
strack_pool = joint_stracks(tracked_stracks, self.lost_stracks)
|
||||
|
||||
# Predict the current location with KF
|
||||
STrack.multi_predict(strack_pool, self.kalman_filter)
|
||||
|
||||
# Fix camera motion
|
||||
if self.camera_motion:
|
||||
warp = self.gmc.apply(img[0], dets)
|
||||
STrack.multi_gmc(strack_pool, warp)
|
||||
STrack.multi_gmc(unconfirmed, warp)
|
||||
|
||||
# Associate with high score detection boxes
|
||||
ious_dists = matching.iou_distance(strack_pool, detections)
|
||||
matches, u_track, u_detection = matching.linear_assignment(
|
||||
ious_dists, thresh=self.match_thresh)
|
||||
|
||||
for itracked, idet in matches:
|
||||
track = strack_pool[itracked]
|
||||
det = detections[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
track.update(detections[idet], self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
else:
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refind_stracks.append(track)
|
||||
''' Step 3: Second association, with low score detection boxes'''
|
||||
if len(scores):
|
||||
inds_high = scores < self.track_high_thresh
|
||||
inds_low = scores > self.track_low_thresh
|
||||
inds_second = np.logical_and(inds_low, inds_high)
|
||||
dets_second = bboxes[inds_second]
|
||||
scores_second = scores[inds_second]
|
||||
classes_second = classes[inds_second]
|
||||
else:
|
||||
dets_second = []
|
||||
scores_second = []
|
||||
classes_second = []
|
||||
|
||||
# association the untrack to the low score detections
|
||||
if len(dets_second) > 0:
|
||||
'''Detections'''
|
||||
detections_second = [
|
||||
STrack(STrack.tlbr_to_tlwh(tlbr), s, c) for (tlbr, s, c) in
|
||||
zip(dets_second, scores_second, classes_second)
|
||||
]
|
||||
else:
|
||||
detections_second = []
|
||||
|
||||
r_tracked_stracks = [
|
||||
strack_pool[i] for i in u_track
|
||||
if strack_pool[i].state == TrackState.Tracked
|
||||
]
|
||||
dists = matching.iou_distance(r_tracked_stracks, detections_second)
|
||||
matches, u_track, u_detection_second = matching.linear_assignment(
|
||||
dists, thresh=0.5)
|
||||
for itracked, idet in matches:
|
||||
track = r_tracked_stracks[itracked]
|
||||
det = detections_second[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
track.update(det, self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
else:
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refind_stracks.append(track)
|
||||
|
||||
for it in u_track:
|
||||
track = r_tracked_stracks[it]
|
||||
if not track.state == TrackState.Lost:
|
||||
track.mark_lost()
|
||||
lost_stracks.append(track)
|
||||
'''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
|
||||
detections = [detections[i] for i in u_detection]
|
||||
dists = matching.iou_distance(unconfirmed, detections)
|
||||
|
||||
matches, u_unconfirmed, u_detection = matching.linear_assignment(
|
||||
dists, thresh=0.7)
|
||||
for itracked, idet in matches:
|
||||
unconfirmed[itracked].update(detections[idet], self.frame_id)
|
||||
activated_starcks.append(unconfirmed[itracked])
|
||||
for it in u_unconfirmed:
|
||||
track = unconfirmed[it]
|
||||
track.mark_removed()
|
||||
removed_stracks.append(track)
|
||||
""" Step 4: Init new stracks"""
|
||||
for inew in u_detection:
|
||||
track = detections[inew]
|
||||
if track.score < self.new_track_thresh:
|
||||
continue
|
||||
|
||||
track.activate(self.kalman_filter, self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
""" Step 5: Update state"""
|
||||
for track in self.lost_stracks:
|
||||
if self.frame_id - track.end_frame > self.max_time_lost:
|
||||
track.mark_removed()
|
||||
removed_stracks.append(track)
|
||||
""" Merge """
|
||||
self.tracked_stracks = [
|
||||
t for t in self.tracked_stracks if t.state == TrackState.Tracked
|
||||
]
|
||||
self.tracked_stracks = joint_stracks(self.tracked_stracks,
|
||||
activated_starcks)
|
||||
self.tracked_stracks = joint_stracks(self.tracked_stracks,
|
||||
refind_stracks)
|
||||
self.lost_stracks = sub_stracks(self.lost_stracks, self.tracked_stracks)
|
||||
self.lost_stracks.extend(lost_stracks)
|
||||
self.lost_stracks = sub_stracks(self.lost_stracks, self.removed_stracks)
|
||||
self.removed_stracks.extend(removed_stracks)
|
||||
self.tracked_stracks, self.lost_stracks = remove_duplicate_stracks(
|
||||
self.tracked_stracks, self.lost_stracks)
|
||||
|
||||
# output_stracks = [track for track in self.tracked_stracks if track.is_activated]
|
||||
output_stracks = [track for track in self.tracked_stracks]
|
||||
|
||||
return output_stracks
|
||||
149
paddle_detection/ppdet/modeling/mot/tracker/center_tracker.py
Normal file
149
paddle_detection/ppdet/modeling/mot/tracker/center_tracker.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/xingyizhou/CenterTrack/blob/master/src/lib/utils/tracker.py
|
||||
"""
|
||||
|
||||
import copy
|
||||
import numpy as np
|
||||
import sklearn
|
||||
|
||||
from ppdet.core.workspace import register, serializable
|
||||
from ppdet.utils.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
__all__ = ['CenterTracker']
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class CenterTracker(object):
|
||||
__shared__ = ['num_classes']
|
||||
|
||||
def __init__(self,
|
||||
num_classes=1,
|
||||
min_box_area=0,
|
||||
vertical_ratio=-1,
|
||||
track_thresh=0.4,
|
||||
pre_thresh=0.5,
|
||||
new_thresh=0.4,
|
||||
out_thresh=0.4,
|
||||
hungarian=False):
|
||||
self.num_classes = num_classes
|
||||
self.min_box_area = min_box_area
|
||||
self.vertical_ratio = vertical_ratio
|
||||
|
||||
self.track_thresh = track_thresh
|
||||
self.pre_thresh = max(track_thresh, pre_thresh)
|
||||
self.new_thresh = max(track_thresh, new_thresh)
|
||||
self.out_thresh = max(track_thresh, out_thresh)
|
||||
self.hungarian = hungarian
|
||||
|
||||
self.reset()
|
||||
|
||||
def init_track(self, results):
|
||||
print('Initialize tracking!')
|
||||
for item in results:
|
||||
if item['score'] > self.new_thresh:
|
||||
self.id_count += 1
|
||||
item['tracking_id'] = self.id_count
|
||||
if not ('ct' in item):
|
||||
bbox = item['bbox']
|
||||
item['ct'] = [(bbox[0] + bbox[2]) / 2,
|
||||
(bbox[1] + bbox[3]) / 2]
|
||||
self.tracks.append(item)
|
||||
|
||||
def reset(self):
|
||||
self.id_count = 0
|
||||
self.tracks = []
|
||||
|
||||
def update(self, results, public_det=None):
|
||||
N = len(results)
|
||||
M = len(self.tracks)
|
||||
|
||||
dets = np.array([det['ct'] + det['tracking'] for det in results],
|
||||
np.float32) # N x 2
|
||||
track_size = np.array([((track['bbox'][2] - track['bbox'][0]) * \
|
||||
(track['bbox'][3] - track['bbox'][1])) \
|
||||
for track in self.tracks], np.float32) # M
|
||||
track_cat = np.array([track['class'] for track in self.tracks],
|
||||
np.int32) # M
|
||||
item_size = np.array([((item['bbox'][2] - item['bbox'][0]) * \
|
||||
(item['bbox'][3] - item['bbox'][1])) \
|
||||
for item in results], np.float32) # N
|
||||
item_cat = np.array([item['class'] for item in results], np.int32) # N
|
||||
tracks = np.array([pre_det['ct'] for pre_det in self.tracks],
|
||||
np.float32) # M x 2
|
||||
dist = (((tracks.reshape(1, -1, 2) - \
|
||||
dets.reshape(-1, 1, 2)) ** 2).sum(axis=2)) # N x M
|
||||
|
||||
invalid = ((dist > track_size.reshape(1, M)) + \
|
||||
(dist > item_size.reshape(N, 1)) + \
|
||||
(item_cat.reshape(N, 1) != track_cat.reshape(1, M))) > 0
|
||||
dist = dist + invalid * 1e18
|
||||
|
||||
if self.hungarian:
|
||||
item_score = np.array([item['score'] for item in results],
|
||||
np.float32)
|
||||
dist[dist > 1e18] = 1e18
|
||||
from sklearn.utils.linear_assignment_ import linear_assignment
|
||||
matched_indices = linear_assignment(dist)
|
||||
else:
|
||||
matched_indices = greedy_assignment(copy.deepcopy(dist))
|
||||
|
||||
unmatched_dets = [d for d in range(dets.shape[0]) \
|
||||
if not (d in matched_indices[:, 0])]
|
||||
unmatched_tracks = [d for d in range(tracks.shape[0]) \
|
||||
if not (d in matched_indices[:, 1])]
|
||||
|
||||
if self.hungarian:
|
||||
matches = []
|
||||
for m in matched_indices:
|
||||
if dist[m[0], m[1]] > 1e16:
|
||||
unmatched_dets.append(m[0])
|
||||
unmatched_tracks.append(m[1])
|
||||
else:
|
||||
matches.append(m)
|
||||
matches = np.array(matches).reshape(-1, 2)
|
||||
else:
|
||||
matches = matched_indices
|
||||
|
||||
ret = []
|
||||
for m in matches:
|
||||
track = results[m[0]]
|
||||
track['tracking_id'] = self.tracks[m[1]]['tracking_id']
|
||||
ret.append(track)
|
||||
|
||||
# Private detection: create tracks for all un-matched detections
|
||||
for i in unmatched_dets:
|
||||
track = results[i]
|
||||
if track['score'] > self.new_thresh:
|
||||
self.id_count += 1
|
||||
track['tracking_id'] = self.id_count
|
||||
ret.append(track)
|
||||
|
||||
self.tracks = ret
|
||||
return ret
|
||||
|
||||
|
||||
def greedy_assignment(dist):
|
||||
matched_indices = []
|
||||
if dist.shape[1] == 0:
|
||||
return np.array(matched_indices, np.int32).reshape(-1, 2)
|
||||
for i in range(dist.shape[0]):
|
||||
j = dist[i].argmin()
|
||||
if dist[i][j] < 1e16:
|
||||
dist[:, j] = 1e18
|
||||
matched_indices.append([i, j])
|
||||
return np.array(matched_indices, np.int32).reshape(-1, 2)
|
||||
189
paddle_detection/ppdet/modeling/mot/tracker/deepsort_tracker.py
Normal file
189
paddle_detection/ppdet/modeling/mot/tracker/deepsort_tracker.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/nwojke/deep_sort/blob/master/deep_sort/tracker.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..motion import KalmanFilter
|
||||
from ..matching.deepsort_matching import NearestNeighborDistanceMetric
|
||||
from ..matching.deepsort_matching import iou_cost, min_cost_matching, matching_cascade, gate_cost_matrix
|
||||
from .base_sde_tracker import Track
|
||||
from ..utils import Detection
|
||||
|
||||
from ppdet.core.workspace import register, serializable
|
||||
from ppdet.utils.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
__all__ = ['DeepSORTTracker']
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class DeepSORTTracker(object):
|
||||
"""
|
||||
DeepSORT tracker
|
||||
|
||||
Args:
|
||||
input_size (list): input feature map size to reid model, [h, w] format,
|
||||
[64, 192] as default.
|
||||
min_box_area (int): min box area to filter out low quality boxes
|
||||
vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
|
||||
bad results, set 1.6 default for pedestrian tracking. If set <=0
|
||||
means no need to filter bboxes.
|
||||
budget (int): If not None, fix samples per class to at most this number.
|
||||
Removes the oldest samples when the budget is reached.
|
||||
max_age (int): maximum number of missed misses before a track is deleted
|
||||
n_init (float): Number of frames that a track remains in initialization
|
||||
phase. Number of consecutive detections before the track is confirmed.
|
||||
The track state is set to `Deleted` if a miss occurs within the first
|
||||
`n_init` frames.
|
||||
metric_type (str): either "euclidean" or "cosine", the distance metric
|
||||
used for measurement to track association.
|
||||
matching_threshold (float): samples with larger distance are
|
||||
considered an invalid match.
|
||||
max_iou_distance (float): max iou distance threshold
|
||||
motion (object): KalmanFilter instance
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
input_size=[64, 192],
|
||||
min_box_area=0,
|
||||
vertical_ratio=-1,
|
||||
budget=100,
|
||||
max_age=70,
|
||||
n_init=3,
|
||||
metric_type='cosine',
|
||||
matching_threshold=0.2,
|
||||
max_iou_distance=0.9,
|
||||
motion='KalmanFilter'):
|
||||
self.input_size = input_size
|
||||
self.min_box_area = min_box_area
|
||||
self.vertical_ratio = vertical_ratio
|
||||
self.max_age = max_age
|
||||
self.n_init = n_init
|
||||
self.metric = NearestNeighborDistanceMetric(metric_type,
|
||||
matching_threshold, budget)
|
||||
self.max_iou_distance = max_iou_distance
|
||||
if motion == 'KalmanFilter':
|
||||
self.motion = KalmanFilter()
|
||||
|
||||
self.tracks = []
|
||||
self._next_id = 1
|
||||
|
||||
def predict(self):
|
||||
"""
|
||||
Propagate track state distributions one time step forward.
|
||||
This function should be called once every time step, before `update`.
|
||||
"""
|
||||
for track in self.tracks:
|
||||
track.predict(self.motion)
|
||||
|
||||
def update(self, pred_dets, pred_embs):
|
||||
"""
|
||||
Perform measurement update and track management.
|
||||
Args:
|
||||
pred_dets (np.array): Detection results of the image, the shape is
|
||||
[N, 6], means 'cls_id, score, x0, y0, x1, y1'.
|
||||
pred_embs (np.array): Embedding results of the image, the shape is
|
||||
[N, 128], usually pred_embs.shape[1] is a multiple of 128.
|
||||
"""
|
||||
pred_cls_ids = pred_dets[:, 0:1]
|
||||
pred_scores = pred_dets[:, 1:2]
|
||||
pred_xyxys = pred_dets[:, 2:6]
|
||||
pred_tlwhs = np.concatenate((pred_xyxys[:, 0:2], pred_xyxys[:, 2:4] - pred_xyxys[:, 0:2] + 1), axis=1)
|
||||
|
||||
detections = [
|
||||
Detection(tlwh, score, feat, cls_id)
|
||||
for tlwh, score, feat, cls_id in zip(pred_tlwhs, pred_scores,
|
||||
pred_embs, pred_cls_ids)
|
||||
]
|
||||
|
||||
# Run matching cascade.
|
||||
matches, unmatched_tracks, unmatched_detections = \
|
||||
self._match(detections)
|
||||
|
||||
# Update track set.
|
||||
for track_idx, detection_idx in matches:
|
||||
self.tracks[track_idx].update(self.motion,
|
||||
detections[detection_idx])
|
||||
for track_idx in unmatched_tracks:
|
||||
self.tracks[track_idx].mark_missed()
|
||||
for detection_idx in unmatched_detections:
|
||||
self._initiate_track(detections[detection_idx])
|
||||
self.tracks = [t for t in self.tracks if not t.is_deleted()]
|
||||
|
||||
# Update distance metric.
|
||||
active_targets = [t.track_id for t in self.tracks if t.is_confirmed()]
|
||||
features, targets = [], []
|
||||
for track in self.tracks:
|
||||
if not track.is_confirmed():
|
||||
continue
|
||||
features += track.features
|
||||
targets += [track.track_id for _ in track.features]
|
||||
track.features = []
|
||||
self.metric.partial_fit(
|
||||
np.asarray(features), np.asarray(targets), active_targets)
|
||||
output_stracks = self.tracks
|
||||
return output_stracks
|
||||
|
||||
def _match(self, detections):
|
||||
def gated_metric(tracks, dets, track_indices, detection_indices):
|
||||
features = np.array([dets[i].feature for i in detection_indices])
|
||||
targets = np.array([tracks[i].track_id for i in track_indices])
|
||||
cost_matrix = self.metric.distance(features, targets)
|
||||
cost_matrix = gate_cost_matrix(self.motion, cost_matrix, tracks,
|
||||
dets, track_indices,
|
||||
detection_indices)
|
||||
return cost_matrix
|
||||
|
||||
# Split track set into confirmed and unconfirmed tracks.
|
||||
confirmed_tracks = [
|
||||
i for i, t in enumerate(self.tracks) if t.is_confirmed()
|
||||
]
|
||||
unconfirmed_tracks = [
|
||||
i for i, t in enumerate(self.tracks) if not t.is_confirmed()
|
||||
]
|
||||
|
||||
# Associate confirmed tracks using appearance features.
|
||||
matches_a, unmatched_tracks_a, unmatched_detections = \
|
||||
matching_cascade(
|
||||
gated_metric, self.metric.matching_threshold, self.max_age,
|
||||
self.tracks, detections, confirmed_tracks)
|
||||
|
||||
# Associate remaining tracks together with unconfirmed tracks using IOU.
|
||||
iou_track_candidates = unconfirmed_tracks + [
|
||||
k for k in unmatched_tracks_a
|
||||
if self.tracks[k].time_since_update == 1
|
||||
]
|
||||
unmatched_tracks_a = [
|
||||
k for k in unmatched_tracks_a
|
||||
if self.tracks[k].time_since_update != 1
|
||||
]
|
||||
matches_b, unmatched_tracks_b, unmatched_detections = \
|
||||
min_cost_matching(
|
||||
iou_cost, self.max_iou_distance, self.tracks,
|
||||
detections, iou_track_candidates, unmatched_detections)
|
||||
|
||||
matches = matches_a + matches_b
|
||||
unmatched_tracks = list(set(unmatched_tracks_a + unmatched_tracks_b))
|
||||
return matches, unmatched_tracks, unmatched_detections
|
||||
|
||||
def _initiate_track(self, detection):
|
||||
mean, covariance = self.motion.initiate(detection.to_xyah())
|
||||
self.tracks.append(
|
||||
Track(mean, covariance, self._next_id, self.n_init, self.max_age,
|
||||
detection.cls_id, detection.score, detection.feature))
|
||||
self._next_id += 1
|
||||
353
paddle_detection/ppdet/modeling/mot/tracker/jde_tracker.py
Normal file
353
paddle_detection/ppdet/modeling/mot/tracker/jde_tracker.py
Normal file
@@ -0,0 +1,353 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/Zhongdao/Towards-Realtime-MOT/blob/master/tracker/multitracker.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
from ..matching import jde_matching as matching
|
||||
from ..motion import KalmanFilter
|
||||
from .base_jde_tracker import TrackState, STrack
|
||||
from .base_jde_tracker import joint_stracks, sub_stracks, remove_duplicate_stracks
|
||||
|
||||
from ppdet.core.workspace import register, serializable
|
||||
from ppdet.utils.logger import setup_logger
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
__all__ = ['JDETracker']
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class JDETracker(object):
|
||||
__shared__ = ['num_classes']
|
||||
"""
|
||||
JDE tracker, support single class and multi classes
|
||||
|
||||
Args:
|
||||
use_byte (bool): Whether use ByteTracker, default False
|
||||
num_classes (int): the number of classes
|
||||
det_thresh (float): threshold of detection score
|
||||
track_buffer (int): buffer for tracker
|
||||
min_box_area (int): min box area to filter out low quality boxes
|
||||
vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
|
||||
bad results. If set <= 0 means no need to filter bboxes,usually set
|
||||
1.6 for pedestrian tracking.
|
||||
tracked_thresh (float): linear assignment threshold of tracked
|
||||
stracks and detections
|
||||
r_tracked_thresh (float): linear assignment threshold of
|
||||
tracked stracks and unmatched detections
|
||||
unconfirmed_thresh (float): linear assignment threshold of
|
||||
unconfirmed stracks and unmatched detections
|
||||
conf_thres (float): confidence threshold for tracking, also used in
|
||||
ByteTracker as higher confidence threshold
|
||||
match_thres (float): linear assignment threshold of tracked
|
||||
stracks and detections in ByteTracker
|
||||
low_conf_thres (float): lower confidence threshold for tracking in
|
||||
ByteTracker
|
||||
input_size (list): input feature map size to reid model, [h, w] format,
|
||||
[64, 192] as default.
|
||||
motion (str): motion model, KalmanFilter as default
|
||||
metric_type (str): either "euclidean" or "cosine", the distance metric
|
||||
used for measurement to track association.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
use_byte=False,
|
||||
num_classes=1,
|
||||
det_thresh=0.3,
|
||||
track_buffer=30,
|
||||
min_box_area=0,
|
||||
vertical_ratio=0,
|
||||
tracked_thresh=0.7,
|
||||
r_tracked_thresh=0.5,
|
||||
unconfirmed_thresh=0.7,
|
||||
conf_thres=0,
|
||||
match_thres=0.8,
|
||||
low_conf_thres=0.2,
|
||||
input_size=[64, 192],
|
||||
motion='KalmanFilter',
|
||||
metric_type='euclidean'):
|
||||
self.use_byte = use_byte
|
||||
self.num_classes = num_classes
|
||||
self.det_thresh = det_thresh if not use_byte else conf_thres + 0.1
|
||||
self.track_buffer = track_buffer
|
||||
self.min_box_area = min_box_area
|
||||
self.vertical_ratio = vertical_ratio
|
||||
|
||||
self.tracked_thresh = tracked_thresh
|
||||
self.r_tracked_thresh = r_tracked_thresh
|
||||
self.unconfirmed_thresh = unconfirmed_thresh
|
||||
self.conf_thres = conf_thres
|
||||
self.match_thres = match_thres
|
||||
self.low_conf_thres = low_conf_thres
|
||||
|
||||
self.input_size = input_size
|
||||
if motion == 'KalmanFilter':
|
||||
self.motion = KalmanFilter()
|
||||
self.metric_type = metric_type
|
||||
|
||||
self.frame_id = 0
|
||||
self.tracked_tracks_dict = defaultdict(list) # dict(list[STrack])
|
||||
self.lost_tracks_dict = defaultdict(list) # dict(list[STrack])
|
||||
self.removed_tracks_dict = defaultdict(list) # dict(list[STrack])
|
||||
|
||||
self.max_time_lost = 0
|
||||
# max_time_lost will be calculated: int(frame_rate / 30.0 * track_buffer)
|
||||
|
||||
def update(self, pred_dets, pred_embs=None):
|
||||
"""
|
||||
Processes the image frame and finds bounding box(detections).
|
||||
Associates the detection with corresponding tracklets and also handles
|
||||
lost, removed, refound and active tracklets.
|
||||
|
||||
Args:
|
||||
pred_dets (np.array): Detection results of the image, the shape is
|
||||
[N, 6], means 'cls_id, score, x0, y0, x1, y1'.
|
||||
pred_embs (np.array): Embedding results of the image, the shape is
|
||||
[N, 128] or [N, 512].
|
||||
|
||||
Return:
|
||||
output_stracks_dict (dict(list)): The list contains information
|
||||
regarding the online_tracklets for the received image tensor.
|
||||
"""
|
||||
self.frame_id += 1
|
||||
if self.frame_id == 1:
|
||||
STrack.init_count(self.num_classes)
|
||||
activated_tracks_dict = defaultdict(list)
|
||||
refined_tracks_dict = defaultdict(list)
|
||||
lost_tracks_dict = defaultdict(list)
|
||||
removed_tracks_dict = defaultdict(list)
|
||||
output_tracks_dict = defaultdict(list)
|
||||
|
||||
pred_dets_dict = defaultdict(list)
|
||||
pred_embs_dict = defaultdict(list)
|
||||
|
||||
# unify single and multi classes detection and embedding results
|
||||
for cls_id in range(self.num_classes):
|
||||
cls_idx = (pred_dets[:, 0:1] == cls_id).squeeze(-1)
|
||||
pred_dets_dict[cls_id] = pred_dets[cls_idx]
|
||||
if pred_embs is not None:
|
||||
pred_embs_dict[cls_id] = pred_embs[cls_idx]
|
||||
else:
|
||||
pred_embs_dict[cls_id] = None
|
||||
|
||||
for cls_id in range(self.num_classes):
|
||||
""" Step 1: Get detections by class"""
|
||||
pred_dets_cls = pred_dets_dict[cls_id]
|
||||
pred_embs_cls = pred_embs_dict[cls_id]
|
||||
remain_inds = (pred_dets_cls[:, 1:2] > self.conf_thres).squeeze(-1)
|
||||
if remain_inds.sum() > 0:
|
||||
pred_dets_cls = pred_dets_cls[remain_inds]
|
||||
if pred_embs_cls is None:
|
||||
# in original ByteTrack
|
||||
detections = [
|
||||
STrack(
|
||||
STrack.tlbr_to_tlwh(tlbrs[2:6]),
|
||||
tlbrs[1],
|
||||
cls_id,
|
||||
30,
|
||||
temp_feat=None) for tlbrs in pred_dets_cls
|
||||
]
|
||||
else:
|
||||
pred_embs_cls = pred_embs_cls[remain_inds]
|
||||
detections = [
|
||||
STrack(
|
||||
STrack.tlbr_to_tlwh(tlbrs[2:6]), tlbrs[1], cls_id,
|
||||
30, temp_feat) for (tlbrs, temp_feat) in
|
||||
zip(pred_dets_cls, pred_embs_cls)
|
||||
]
|
||||
else:
|
||||
detections = []
|
||||
''' Add newly detected tracklets to tracked_stracks'''
|
||||
unconfirmed_dict = defaultdict(list)
|
||||
tracked_tracks_dict = defaultdict(list)
|
||||
for track in self.tracked_tracks_dict[cls_id]:
|
||||
if not track.is_activated:
|
||||
# previous tracks which are not active in the current frame are added in unconfirmed list
|
||||
unconfirmed_dict[cls_id].append(track)
|
||||
else:
|
||||
# Active tracks are added to the local list 'tracked_stracks'
|
||||
tracked_tracks_dict[cls_id].append(track)
|
||||
""" Step 2: First association, with embedding"""
|
||||
# building tracking pool for the current frame
|
||||
track_pool_dict = defaultdict(list)
|
||||
track_pool_dict[cls_id] = joint_stracks(
|
||||
tracked_tracks_dict[cls_id], self.lost_tracks_dict[cls_id])
|
||||
|
||||
# Predict the current location with KalmanFilter
|
||||
STrack.multi_predict(track_pool_dict[cls_id], self.motion)
|
||||
|
||||
if pred_embs_cls is None:
|
||||
# in original ByteTrack
|
||||
dists = matching.iou_distance(track_pool_dict[cls_id],
|
||||
detections)
|
||||
matches, u_track, u_detection = matching.linear_assignment(
|
||||
dists, thresh=self.match_thres) # not self.tracked_thresh
|
||||
else:
|
||||
dists = matching.embedding_distance(
|
||||
track_pool_dict[cls_id],
|
||||
detections,
|
||||
metric=self.metric_type)
|
||||
dists = matching.fuse_motion(
|
||||
self.motion, dists, track_pool_dict[cls_id], detections)
|
||||
matches, u_track, u_detection = matching.linear_assignment(
|
||||
dists, thresh=self.tracked_thresh)
|
||||
|
||||
for i_tracked, idet in matches:
|
||||
# i_tracked is the id of the track and idet is the detection
|
||||
track = track_pool_dict[cls_id][i_tracked]
|
||||
det = detections[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
# If the track is active, add the detection to the track
|
||||
track.update(detections[idet], self.frame_id)
|
||||
activated_tracks_dict[cls_id].append(track)
|
||||
else:
|
||||
# We have obtained a detection from a track which is not active,
|
||||
# hence put the track in refind_stracks list
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refined_tracks_dict[cls_id].append(track)
|
||||
|
||||
# None of the steps below happen if there are no undetected tracks.
|
||||
""" Step 3: Second association, with IOU"""
|
||||
if self.use_byte:
|
||||
inds_low = pred_dets_dict[cls_id][:, 1:2] > self.low_conf_thres
|
||||
inds_high = pred_dets_dict[cls_id][:, 1:2] < self.conf_thres
|
||||
inds_second = np.logical_and(inds_low, inds_high).squeeze(-1)
|
||||
pred_dets_cls_second = pred_dets_dict[cls_id][inds_second]
|
||||
|
||||
# association the untrack to the low score detections
|
||||
if len(pred_dets_cls_second) > 0:
|
||||
if pred_embs_dict[cls_id] is None:
|
||||
# in original ByteTrack
|
||||
detections_second = [
|
||||
STrack(
|
||||
STrack.tlbr_to_tlwh(tlbrs[2:6]),
|
||||
tlbrs[1],
|
||||
cls_id,
|
||||
30,
|
||||
temp_feat=None)
|
||||
for tlbrs in pred_dets_cls_second
|
||||
]
|
||||
else:
|
||||
pred_embs_cls_second = pred_embs_dict[cls_id][
|
||||
inds_second]
|
||||
detections_second = [
|
||||
STrack(
|
||||
STrack.tlbr_to_tlwh(tlbrs[2:6]), tlbrs[1],
|
||||
cls_id, 30, temp_feat) for (tlbrs, temp_feat) in
|
||||
zip(pred_dets_cls_second, pred_embs_cls_second)
|
||||
]
|
||||
else:
|
||||
detections_second = []
|
||||
r_tracked_stracks = [
|
||||
track_pool_dict[cls_id][i] for i in u_track
|
||||
if track_pool_dict[cls_id][i].state == TrackState.Tracked
|
||||
]
|
||||
dists = matching.iou_distance(r_tracked_stracks,
|
||||
detections_second)
|
||||
matches, u_track, u_detection_second = matching.linear_assignment(
|
||||
dists, thresh=0.4) # not r_tracked_thresh
|
||||
else:
|
||||
detections = [detections[i] for i in u_detection]
|
||||
r_tracked_stracks = []
|
||||
for i in u_track:
|
||||
if track_pool_dict[cls_id][i].state == TrackState.Tracked:
|
||||
r_tracked_stracks.append(track_pool_dict[cls_id][i])
|
||||
dists = matching.iou_distance(r_tracked_stracks, detections)
|
||||
|
||||
matches, u_track, u_detection = matching.linear_assignment(
|
||||
dists, thresh=self.r_tracked_thresh)
|
||||
|
||||
for i_tracked, idet in matches:
|
||||
track = r_tracked_stracks[i_tracked]
|
||||
det = detections[
|
||||
idet] if not self.use_byte else detections_second[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
track.update(det, self.frame_id)
|
||||
activated_tracks_dict[cls_id].append(track)
|
||||
else:
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refined_tracks_dict[cls_id].append(track)
|
||||
|
||||
for it in u_track:
|
||||
track = r_tracked_stracks[it]
|
||||
if not track.state == TrackState.Lost:
|
||||
track.mark_lost()
|
||||
lost_tracks_dict[cls_id].append(track)
|
||||
'''Deal with unconfirmed tracks, usually tracks with only one beginning frame'''
|
||||
detections = [detections[i] for i in u_detection]
|
||||
dists = matching.iou_distance(unconfirmed_dict[cls_id], detections)
|
||||
matches, u_unconfirmed, u_detection = matching.linear_assignment(
|
||||
dists, thresh=self.unconfirmed_thresh)
|
||||
for i_tracked, idet in matches:
|
||||
unconfirmed_dict[cls_id][i_tracked].update(detections[idet],
|
||||
self.frame_id)
|
||||
activated_tracks_dict[cls_id].append(unconfirmed_dict[cls_id][
|
||||
i_tracked])
|
||||
for it in u_unconfirmed:
|
||||
track = unconfirmed_dict[cls_id][it]
|
||||
track.mark_removed()
|
||||
removed_tracks_dict[cls_id].append(track)
|
||||
""" Step 4: Init new stracks"""
|
||||
for inew in u_detection:
|
||||
track = detections[inew]
|
||||
if track.score < self.det_thresh:
|
||||
continue
|
||||
track.activate(self.motion, self.frame_id)
|
||||
activated_tracks_dict[cls_id].append(track)
|
||||
""" Step 5: Update state"""
|
||||
for track in self.lost_tracks_dict[cls_id]:
|
||||
if self.frame_id - track.end_frame > self.max_time_lost:
|
||||
track.mark_removed()
|
||||
removed_tracks_dict[cls_id].append(track)
|
||||
|
||||
self.tracked_tracks_dict[cls_id] = [
|
||||
t for t in self.tracked_tracks_dict[cls_id]
|
||||
if t.state == TrackState.Tracked
|
||||
]
|
||||
self.tracked_tracks_dict[cls_id] = joint_stracks(
|
||||
self.tracked_tracks_dict[cls_id], activated_tracks_dict[cls_id])
|
||||
self.tracked_tracks_dict[cls_id] = joint_stracks(
|
||||
self.tracked_tracks_dict[cls_id], refined_tracks_dict[cls_id])
|
||||
self.lost_tracks_dict[cls_id] = sub_stracks(
|
||||
self.lost_tracks_dict[cls_id], self.tracked_tracks_dict[cls_id])
|
||||
self.lost_tracks_dict[cls_id].extend(lost_tracks_dict[cls_id])
|
||||
self.lost_tracks_dict[cls_id] = sub_stracks(
|
||||
self.lost_tracks_dict[cls_id], self.removed_tracks_dict[cls_id])
|
||||
self.removed_tracks_dict[cls_id].extend(removed_tracks_dict[cls_id])
|
||||
self.tracked_tracks_dict[cls_id], self.lost_tracks_dict[
|
||||
cls_id] = remove_duplicate_stracks(
|
||||
self.tracked_tracks_dict[cls_id],
|
||||
self.lost_tracks_dict[cls_id])
|
||||
|
||||
# get scores of lost tracks
|
||||
output_tracks_dict[cls_id] = [
|
||||
track for track in self.tracked_tracks_dict[cls_id]
|
||||
if track.is_activated
|
||||
]
|
||||
|
||||
logger.debug('===========Frame {}=========='.format(self.frame_id))
|
||||
logger.debug('Activated: {}'.format(
|
||||
[track.track_id for track in activated_tracks_dict[cls_id]]))
|
||||
logger.debug('Refind: {}'.format(
|
||||
[track.track_id for track in refined_tracks_dict[cls_id]]))
|
||||
logger.debug('Lost: {}'.format(
|
||||
[track.track_id for track in lost_tracks_dict[cls_id]]))
|
||||
logger.debug('Removed: {}'.format(
|
||||
[track.track_id for track in removed_tracks_dict[cls_id]]))
|
||||
|
||||
return output_tracks_dict
|
||||
371
paddle_detection/ppdet/modeling/mot/tracker/ocsort_tracker.py
Normal file
371
paddle_detection/ppdet/modeling/mot/tracker/ocsort_tracker.py
Normal file
@@ -0,0 +1,371 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
This code is based on https://github.com/noahcao/OC_SORT/blob/master/trackers/ocsort_tracker/ocsort.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from ..matching.ocsort_matching import associate, linear_assignment, iou_batch, associate_only_iou
|
||||
from ..motion.ocsort_kalman_filter import OCSORTKalmanFilter
|
||||
from ppdet.core.workspace import register, serializable
|
||||
|
||||
|
||||
def k_previous_obs(observations, cur_age, k):
|
||||
if len(observations) == 0:
|
||||
return [-1, -1, -1, -1, -1]
|
||||
for i in range(k):
|
||||
dt = k - i
|
||||
if cur_age - dt in observations:
|
||||
return observations[cur_age - dt]
|
||||
max_age = max(observations.keys())
|
||||
return observations[max_age]
|
||||
|
||||
|
||||
def convert_bbox_to_z(bbox):
|
||||
"""
|
||||
Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form
|
||||
[x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is
|
||||
the aspect ratio
|
||||
"""
|
||||
w = bbox[2] - bbox[0]
|
||||
h = bbox[3] - bbox[1]
|
||||
x = bbox[0] + w / 2.
|
||||
y = bbox[1] + h / 2.
|
||||
s = w * h # scale is just area
|
||||
r = w / float(h + 1e-6)
|
||||
return np.array([x, y, s, r]).reshape((4, 1))
|
||||
|
||||
|
||||
def convert_x_to_bbox(x, score=None):
|
||||
"""
|
||||
Takes a bounding box in the centre form [x,y,s,r] and returns it in the form
|
||||
[x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right
|
||||
"""
|
||||
w = np.sqrt(x[2] * x[3])
|
||||
h = x[2] / w
|
||||
if (score == None):
|
||||
return np.array(
|
||||
[x[0] - w / 2., x[1] - h / 2., x[0] + w / 2.,
|
||||
x[1] + h / 2.]).reshape((1, 4))
|
||||
else:
|
||||
score = np.array([score])
|
||||
return np.array([
|
||||
x[0] - w / 2., x[1] - h / 2., x[0] + w / 2., x[1] + h / 2., score
|
||||
]).reshape((1, 5))
|
||||
|
||||
|
||||
def speed_direction(bbox1, bbox2):
|
||||
cx1, cy1 = (bbox1[0] + bbox1[2]) / 2.0, (bbox1[1] + bbox1[3]) / 2.0
|
||||
cx2, cy2 = (bbox2[0] + bbox2[2]) / 2.0, (bbox2[1] + bbox2[3]) / 2.0
|
||||
speed = np.array([cy2 - cy1, cx2 - cx1])
|
||||
norm = np.sqrt((cy2 - cy1)**2 + (cx2 - cx1)**2) + 1e-6
|
||||
return speed / norm
|
||||
|
||||
|
||||
class KalmanBoxTracker(object):
|
||||
"""
|
||||
This class represents the internal state of individual tracked objects observed as bbox.
|
||||
|
||||
Args:
|
||||
bbox (np.array): bbox in [x1,y1,x2,y2,score] format.
|
||||
delta_t (int): delta_t of previous observation
|
||||
"""
|
||||
count = 0
|
||||
|
||||
def __init__(self, bbox, delta_t=3):
|
||||
|
||||
self.kf = OCSORTKalmanFilter(dim_x=7, dim_z=4)
|
||||
self.kf.F = np.array([[1., 0, 0, 0, 1., 0, 0], [0, 1., 0, 0, 0, 1., 0],
|
||||
[0, 0, 1., 0, 0, 0, 1], [0, 0, 0, 1., 0, 0, 0],
|
||||
[0, 0, 0, 0, 1., 0, 0], [0, 0, 0, 0, 0, 1., 0],
|
||||
[0, 0, 0, 0, 0, 0, 1.]])
|
||||
self.kf.H = np.array([[1., 0, 0, 0, 0, 0, 0], [0, 1., 0, 0, 0, 0, 0],
|
||||
[0, 0, 1., 0, 0, 0, 0], [0, 0, 0, 1., 0, 0, 0]])
|
||||
self.kf.R[2:, 2:] *= 10.
|
||||
self.kf.P[4:, 4:] *= 1000.
|
||||
# give high uncertainty to the unobservable initial velocities
|
||||
self.kf.P *= 10.
|
||||
self.kf.Q[-1, -1] *= 0.01
|
||||
self.kf.Q[4:, 4:] *= 0.01
|
||||
|
||||
self.score = bbox[4]
|
||||
self.kf.x[:4] = convert_bbox_to_z(bbox)
|
||||
self.time_since_update = 0
|
||||
self.id = KalmanBoxTracker.count
|
||||
KalmanBoxTracker.count += 1
|
||||
self.history = []
|
||||
self.hits = 0
|
||||
self.hit_streak = 0
|
||||
self.age = 0
|
||||
"""
|
||||
NOTE: [-1,-1,-1,-1,-1] is a compromising placeholder for non-observation status, the same for the return of
|
||||
function k_previous_obs. It is ugly and I do not like it. But to support generate observation array in a
|
||||
fast and unified way, which you would see below k_observations = np.array([k_previous_obs(...]]), let's bear it for now.
|
||||
"""
|
||||
self.last_observation = np.array([-1, -1, -1, -1, -1]) # placeholder
|
||||
self.observations = dict()
|
||||
self.history_observations = []
|
||||
self.velocity = None
|
||||
self.delta_t = delta_t
|
||||
|
||||
def update(self, bbox, angle_cost=False):
|
||||
"""
|
||||
Updates the state vector with observed bbox.
|
||||
"""
|
||||
if bbox is not None:
|
||||
if angle_cost and self.last_observation.sum(
|
||||
) >= 0: # no previous observation
|
||||
previous_box = None
|
||||
for i in range(self.delta_t):
|
||||
dt = self.delta_t - i
|
||||
if self.age - dt in self.observations:
|
||||
previous_box = self.observations[self.age - dt]
|
||||
break
|
||||
if previous_box is None:
|
||||
previous_box = self.last_observation
|
||||
"""
|
||||
Estimate the track speed direction with observations \Delta t steps away
|
||||
"""
|
||||
self.velocity = speed_direction(previous_box, bbox)
|
||||
"""
|
||||
Insert new observations. This is a ugly way to maintain both self.observations
|
||||
and self.history_observations. Bear it for the moment.
|
||||
"""
|
||||
self.last_observation = bbox
|
||||
self.observations[self.age] = bbox
|
||||
self.history_observations.append(bbox)
|
||||
|
||||
self.time_since_update = 0
|
||||
self.history = []
|
||||
self.hits += 1
|
||||
self.hit_streak += 1
|
||||
self.kf.update(convert_bbox_to_z(bbox))
|
||||
else:
|
||||
self.kf.update(bbox)
|
||||
|
||||
def predict(self):
|
||||
"""
|
||||
Advances the state vector and returns the predicted bounding box estimate.
|
||||
"""
|
||||
if ((self.kf.x[6] + self.kf.x[2]) <= 0):
|
||||
self.kf.x[6] *= 0.0
|
||||
|
||||
self.kf.predict()
|
||||
self.age += 1
|
||||
if (self.time_since_update > 0):
|
||||
self.hit_streak = 0
|
||||
self.time_since_update += 1
|
||||
self.history.append(convert_x_to_bbox(self.kf.x, score=self.score))
|
||||
return self.history[-1]
|
||||
|
||||
def get_state(self):
|
||||
return convert_x_to_bbox(self.kf.x, score=self.score)
|
||||
|
||||
|
||||
@register
|
||||
@serializable
|
||||
class OCSORTTracker(object):
|
||||
"""
|
||||
OCSORT tracker, support single class
|
||||
|
||||
Args:
|
||||
det_thresh (float): threshold of detection score
|
||||
max_age (int): maximum number of missed misses before a track is deleted
|
||||
min_hits (int): minimum hits for associate
|
||||
iou_threshold (float): iou threshold for associate
|
||||
delta_t (int): delta_t of previous observation
|
||||
inertia (float): vdc_weight of angle_diff_cost for associate
|
||||
vertical_ratio (float): w/h, the vertical ratio of the bbox to filter
|
||||
bad results. If set <= 0 means no need to filter bboxes,usually set
|
||||
1.6 for pedestrian tracking.
|
||||
min_box_area (int): min box area to filter out low quality boxes
|
||||
use_byte (bool): Whether use ByteTracker, default False
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
det_thresh=0.6,
|
||||
max_age=30,
|
||||
min_hits=3,
|
||||
iou_threshold=0.3,
|
||||
delta_t=3,
|
||||
inertia=0.2,
|
||||
vertical_ratio=-1,
|
||||
min_box_area=0,
|
||||
use_byte=False,
|
||||
use_angle_cost=False):
|
||||
self.det_thresh = det_thresh
|
||||
self.max_age = max_age
|
||||
self.min_hits = min_hits
|
||||
self.iou_threshold = iou_threshold
|
||||
self.delta_t = delta_t
|
||||
self.inertia = inertia
|
||||
self.vertical_ratio = vertical_ratio
|
||||
self.min_box_area = min_box_area
|
||||
self.use_byte = use_byte
|
||||
self.use_angle_cost = use_angle_cost
|
||||
|
||||
self.trackers = []
|
||||
self.frame_count = 0
|
||||
KalmanBoxTracker.count = 0
|
||||
|
||||
def update(self, pred_dets, pred_embs=None):
|
||||
"""
|
||||
Args:
|
||||
pred_dets (np.array): Detection results of the image, the shape is
|
||||
[N, 6], means 'cls_id, score, x0, y0, x1, y1'.
|
||||
pred_embs (np.array): Embedding results of the image, the shape is
|
||||
[N, 128] or [N, 512], default as None.
|
||||
|
||||
Return:
|
||||
tracking boxes (np.array): [M, 6], means 'x0, y0, x1, y1, score, id'.
|
||||
"""
|
||||
if pred_dets is None:
|
||||
return np.empty((0, 6))
|
||||
|
||||
self.frame_count += 1
|
||||
|
||||
bboxes = pred_dets[:, 2:]
|
||||
scores = pred_dets[:, 1:2]
|
||||
dets = np.concatenate((bboxes, scores), axis=1)
|
||||
scores = scores.squeeze(-1)
|
||||
|
||||
inds_low = scores > 0.1
|
||||
inds_high = scores < self.det_thresh
|
||||
inds_second = np.logical_and(inds_low, inds_high)
|
||||
# self.det_thresh > score > 0.1, for second matching
|
||||
dets_second = dets[inds_second] # detections for second matching
|
||||
remain_inds = scores > self.det_thresh
|
||||
dets = dets[remain_inds]
|
||||
|
||||
# get predicted locations from existing trackers.
|
||||
trks = np.zeros((len(self.trackers), 5))
|
||||
to_del = []
|
||||
ret = []
|
||||
for t, trk in enumerate(trks):
|
||||
pos = self.trackers[t].predict()[0]
|
||||
trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
|
||||
if np.any(np.isnan(pos)):
|
||||
to_del.append(t)
|
||||
trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
|
||||
for t in reversed(to_del):
|
||||
self.trackers.pop(t)
|
||||
|
||||
if self.use_angle_cost:
|
||||
velocities = np.array([
|
||||
trk.velocity if trk.velocity is not None else np.array((0, 0))
|
||||
for trk in self.trackers
|
||||
])
|
||||
|
||||
k_observations = np.array([
|
||||
k_previous_obs(trk.observations, trk.age, self.delta_t)
|
||||
for trk in self.trackers
|
||||
])
|
||||
last_boxes = np.array([trk.last_observation for trk in self.trackers])
|
||||
"""
|
||||
First round of association
|
||||
"""
|
||||
if self.use_angle_cost:
|
||||
matched, unmatched_dets, unmatched_trks = associate(
|
||||
dets, trks, self.iou_threshold, velocities, k_observations,
|
||||
self.inertia)
|
||||
else:
|
||||
matched, unmatched_dets, unmatched_trks = associate_only_iou(
|
||||
dets, trks, self.iou_threshold)
|
||||
|
||||
for m in matched:
|
||||
self.trackers[m[1]].update(
|
||||
dets[m[0], :], angle_cost=self.use_angle_cost)
|
||||
"""
|
||||
Second round of associaton by OCR
|
||||
"""
|
||||
# BYTE association
|
||||
if self.use_byte and len(dets_second) > 0 and unmatched_trks.shape[
|
||||
0] > 0:
|
||||
u_trks = trks[unmatched_trks]
|
||||
iou_left = iou_batch(
|
||||
dets_second,
|
||||
u_trks) # iou between low score detections and unmatched tracks
|
||||
iou_left = np.array(iou_left)
|
||||
if iou_left.max() > self.iou_threshold:
|
||||
"""
|
||||
NOTE: by using a lower threshold, e.g., self.iou_threshold - 0.1, you may
|
||||
get a higher performance especially on MOT17/MOT20 datasets. But we keep it
|
||||
uniform here for simplicity
|
||||
"""
|
||||
matched_indices = linear_assignment(-iou_left)
|
||||
to_remove_trk_indices = []
|
||||
for m in matched_indices:
|
||||
det_ind, trk_ind = m[0], unmatched_trks[m[1]]
|
||||
if iou_left[m[0], m[1]] < self.iou_threshold:
|
||||
continue
|
||||
self.trackers[trk_ind].update(
|
||||
dets_second[det_ind, :], angle_cost=self.use_angle_cost)
|
||||
to_remove_trk_indices.append(trk_ind)
|
||||
unmatched_trks = np.setdiff1d(unmatched_trks,
|
||||
np.array(to_remove_trk_indices))
|
||||
|
||||
if unmatched_dets.shape[0] > 0 and unmatched_trks.shape[0] > 0:
|
||||
left_dets = dets[unmatched_dets]
|
||||
left_trks = last_boxes[unmatched_trks]
|
||||
iou_left = iou_batch(left_dets, left_trks)
|
||||
iou_left = np.array(iou_left)
|
||||
if iou_left.max() > self.iou_threshold:
|
||||
"""
|
||||
NOTE: by using a lower threshold, e.g., self.iou_threshold - 0.1, you may
|
||||
get a higher performance especially on MOT17/MOT20 datasets. But we keep it
|
||||
uniform here for simplicity
|
||||
"""
|
||||
rematched_indices = linear_assignment(-iou_left)
|
||||
to_remove_det_indices = []
|
||||
to_remove_trk_indices = []
|
||||
for m in rematched_indices:
|
||||
det_ind, trk_ind = unmatched_dets[m[0]], unmatched_trks[m[
|
||||
1]]
|
||||
if iou_left[m[0], m[1]] < self.iou_threshold:
|
||||
continue
|
||||
self.trackers[trk_ind].update(
|
||||
dets[det_ind, :], angle_cost=self.use_angle_cost)
|
||||
to_remove_det_indices.append(det_ind)
|
||||
to_remove_trk_indices.append(trk_ind)
|
||||
unmatched_dets = np.setdiff1d(unmatched_dets,
|
||||
np.array(to_remove_det_indices))
|
||||
unmatched_trks = np.setdiff1d(unmatched_trks,
|
||||
np.array(to_remove_trk_indices))
|
||||
|
||||
for m in unmatched_trks:
|
||||
self.trackers[m].update(None)
|
||||
|
||||
# create and initialise new trackers for unmatched detections
|
||||
for i in unmatched_dets:
|
||||
trk = KalmanBoxTracker(dets[i, :], delta_t=self.delta_t)
|
||||
self.trackers.append(trk)
|
||||
|
||||
i = len(self.trackers)
|
||||
for trk in reversed(self.trackers):
|
||||
if trk.last_observation.sum() < 0:
|
||||
d = trk.get_state()[0]
|
||||
else:
|
||||
d = trk.last_observation # tlbr + score
|
||||
if (trk.time_since_update < 1) and (
|
||||
trk.hit_streak >= self.min_hits or
|
||||
self.frame_count <= self.min_hits):
|
||||
# +1 as MOT benchmark requires positive
|
||||
ret.append(np.concatenate((d, [trk.id + 1])).reshape(1, -1))
|
||||
i -= 1
|
||||
# remove dead tracklet
|
||||
if (trk.time_since_update > self.max_age):
|
||||
self.trackers.pop(i)
|
||||
if (len(ret) > 0):
|
||||
return np.concatenate(ret)
|
||||
return np.empty((0, 6))
|
||||
Reference in New Issue
Block a user