add StrongSORT Tacker
This commit is contained in:
parent
ffc2e99678
commit
b7d8b3266f
93 changed files with 20230 additions and 6 deletions
377
feeder/trackers/ocsort/association.py
Normal file
377
feeder/trackers/ocsort/association.py
Normal file
|
@ -0,0 +1,377 @@
|
|||
import os
|
||||
import numpy as np
|
||||
|
||||
|
||||
def iou_batch(bboxes1, bboxes2):
|
||||
"""
|
||||
From SORT: Computes IOU between two bboxes in the form [x1,y1,x2,y2]
|
||||
"""
|
||||
bboxes2 = np.expand_dims(bboxes2, 0)
|
||||
bboxes1 = np.expand_dims(bboxes1, 1)
|
||||
|
||||
xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
w = np.maximum(0., xx2 - xx1)
|
||||
h = np.maximum(0., yy2 - yy1)
|
||||
wh = w * h
|
||||
o = wh / ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
return(o)
|
||||
|
||||
|
||||
def giou_batch(bboxes1, bboxes2):
|
||||
"""
|
||||
:param bbox_p: predict of bbox(N,4)(x1,y1,x2,y2)
|
||||
:param bbox_g: groundtruth of bbox(N,4)(x1,y1,x2,y2)
|
||||
:return:
|
||||
"""
|
||||
# for details should go to https://arxiv.org/pdf/1902.09630.pdf
|
||||
# ensure predict's bbox form
|
||||
bboxes2 = np.expand_dims(bboxes2, 0)
|
||||
bboxes1 = np.expand_dims(bboxes1, 1)
|
||||
|
||||
xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
w = np.maximum(0., xx2 - xx1)
|
||||
h = np.maximum(0., yy2 - yy1)
|
||||
wh = w * h
|
||||
iou = wh / ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
|
||||
xxc1 = np.minimum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yyc1 = np.minimum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xxc2 = np.maximum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yyc2 = np.maximum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
wc = xxc2 - xxc1
|
||||
hc = yyc2 - yyc1
|
||||
assert((wc > 0).all() and (hc > 0).all())
|
||||
area_enclose = wc * hc
|
||||
giou = iou - (area_enclose - wh) / area_enclose
|
||||
giou = (giou + 1.)/2.0 # resize from (-1,1) to (0,1)
|
||||
return giou
|
||||
|
||||
|
||||
def diou_batch(bboxes1, bboxes2):
|
||||
"""
|
||||
:param bbox_p: predict of bbox(N,4)(x1,y1,x2,y2)
|
||||
:param bbox_g: groundtruth of bbox(N,4)(x1,y1,x2,y2)
|
||||
:return:
|
||||
"""
|
||||
# for details should go to https://arxiv.org/pdf/1902.09630.pdf
|
||||
# ensure predict's bbox form
|
||||
bboxes2 = np.expand_dims(bboxes2, 0)
|
||||
bboxes1 = np.expand_dims(bboxes1, 1)
|
||||
|
||||
# calculate the intersection box
|
||||
xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
w = np.maximum(0., xx2 - xx1)
|
||||
h = np.maximum(0., yy2 - yy1)
|
||||
wh = w * h
|
||||
iou = wh / ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
|
||||
centerx1 = (bboxes1[..., 0] + bboxes1[..., 2]) / 2.0
|
||||
centery1 = (bboxes1[..., 1] + bboxes1[..., 3]) / 2.0
|
||||
centerx2 = (bboxes2[..., 0] + bboxes2[..., 2]) / 2.0
|
||||
centery2 = (bboxes2[..., 1] + bboxes2[..., 3]) / 2.0
|
||||
|
||||
inner_diag = (centerx1 - centerx2) ** 2 + (centery1 - centery2) ** 2
|
||||
|
||||
xxc1 = np.minimum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yyc1 = np.minimum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xxc2 = np.maximum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yyc2 = np.maximum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
|
||||
outer_diag = (xxc2 - xxc1) ** 2 + (yyc2 - yyc1) ** 2
|
||||
diou = iou - inner_diag / outer_diag
|
||||
|
||||
return (diou + 1) / 2.0 # resize from (-1,1) to (0,1)
|
||||
|
||||
def ciou_batch(bboxes1, bboxes2):
|
||||
"""
|
||||
:param bbox_p: predict of bbox(N,4)(x1,y1,x2,y2)
|
||||
:param bbox_g: groundtruth of bbox(N,4)(x1,y1,x2,y2)
|
||||
:return:
|
||||
"""
|
||||
# for details should go to https://arxiv.org/pdf/1902.09630.pdf
|
||||
# ensure predict's bbox form
|
||||
bboxes2 = np.expand_dims(bboxes2, 0)
|
||||
bboxes1 = np.expand_dims(bboxes1, 1)
|
||||
|
||||
# calculate the intersection box
|
||||
xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
w = np.maximum(0., xx2 - xx1)
|
||||
h = np.maximum(0., yy2 - yy1)
|
||||
wh = w * h
|
||||
iou = wh / ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
|
||||
centerx1 = (bboxes1[..., 0] + bboxes1[..., 2]) / 2.0
|
||||
centery1 = (bboxes1[..., 1] + bboxes1[..., 3]) / 2.0
|
||||
centerx2 = (bboxes2[..., 0] + bboxes2[..., 2]) / 2.0
|
||||
centery2 = (bboxes2[..., 1] + bboxes2[..., 3]) / 2.0
|
||||
|
||||
inner_diag = (centerx1 - centerx2) ** 2 + (centery1 - centery2) ** 2
|
||||
|
||||
xxc1 = np.minimum(bboxes1[..., 0], bboxes2[..., 0])
|
||||
yyc1 = np.minimum(bboxes1[..., 1], bboxes2[..., 1])
|
||||
xxc2 = np.maximum(bboxes1[..., 2], bboxes2[..., 2])
|
||||
yyc2 = np.maximum(bboxes1[..., 3], bboxes2[..., 3])
|
||||
|
||||
outer_diag = (xxc2 - xxc1) ** 2 + (yyc2 - yyc1) ** 2
|
||||
|
||||
w1 = bboxes1[..., 2] - bboxes1[..., 0]
|
||||
h1 = bboxes1[..., 3] - bboxes1[..., 1]
|
||||
w2 = bboxes2[..., 2] - bboxes2[..., 0]
|
||||
h2 = bboxes2[..., 3] - bboxes2[..., 1]
|
||||
|
||||
# prevent dividing over zero. add one pixel shift
|
||||
h2 = h2 + 1.
|
||||
h1 = h1 + 1.
|
||||
arctan = np.arctan(w2/h2) - np.arctan(w1/h1)
|
||||
v = (4 / (np.pi ** 2)) * (arctan ** 2)
|
||||
S = 1 - iou
|
||||
alpha = v / (S+v)
|
||||
ciou = iou - inner_diag / outer_diag - alpha * v
|
||||
|
||||
return (ciou + 1) / 2.0 # resize from (-1,1) to (0,1)
|
||||
|
||||
|
||||
def ct_dist(bboxes1, bboxes2):
|
||||
"""
|
||||
Measure the center distance between two sets of bounding boxes,
|
||||
this is a coarse implementation, we don't recommend using it only
|
||||
for association, which can be unstable and sensitive to frame rate
|
||||
and object speed.
|
||||
"""
|
||||
bboxes2 = np.expand_dims(bboxes2, 0)
|
||||
bboxes1 = np.expand_dims(bboxes1, 1)
|
||||
|
||||
centerx1 = (bboxes1[..., 0] + bboxes1[..., 2]) / 2.0
|
||||
centery1 = (bboxes1[..., 1] + bboxes1[..., 3]) / 2.0
|
||||
centerx2 = (bboxes2[..., 0] + bboxes2[..., 2]) / 2.0
|
||||
centery2 = (bboxes2[..., 1] + bboxes2[..., 3]) / 2.0
|
||||
|
||||
ct_dist2 = (centerx1 - centerx2) ** 2 + (centery1 - centery2) ** 2
|
||||
|
||||
ct_dist = np.sqrt(ct_dist2)
|
||||
|
||||
# The linear rescaling is a naive version and needs more study
|
||||
ct_dist = ct_dist / ct_dist.max()
|
||||
return ct_dist.max() - ct_dist # resize to (0,1)
|
||||
|
||||
|
||||
|
||||
def speed_direction_batch(dets, tracks):
|
||||
tracks = tracks[..., np.newaxis]
|
||||
CX1, CY1 = (dets[:,0] + dets[:,2])/2.0, (dets[:,1]+dets[:,3])/2.0
|
||||
CX2, CY2 = (tracks[:,0] + tracks[:,2]) /2.0, (tracks[:,1]+tracks[:,3])/2.0
|
||||
dx = CX1 - CX2
|
||||
dy = CY1 - CY2
|
||||
norm = np.sqrt(dx**2 + dy**2) + 1e-6
|
||||
dx = dx / norm
|
||||
dy = dy / norm
|
||||
return dy, dx # size: num_track x num_det
|
||||
|
||||
|
||||
def linear_assignment(cost_matrix):
|
||||
try:
|
||||
import lap
|
||||
_, x, y = lap.lapjv(cost_matrix, extend_cost=True)
|
||||
return np.array([[y[i],i] for i in x if i >= 0]) #
|
||||
except ImportError:
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
x, y = linear_sum_assignment(cost_matrix)
|
||||
return np.array(list(zip(x, y)))
|
||||
|
||||
|
||||
def associate_detections_to_trackers(detections,trackers, iou_threshold = 0.3):
|
||||
"""
|
||||
Assigns detections to tracked object (both represented as bounding boxes)
|
||||
Returns 3 lists of matches, unmatched_detections and unmatched_trackers
|
||||
"""
|
||||
if(len(trackers)==0):
|
||||
return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)
|
||||
|
||||
iou_matrix = iou_batch(detections, trackers)
|
||||
|
||||
if min(iou_matrix.shape) > 0:
|
||||
a = (iou_matrix > iou_threshold).astype(np.int32)
|
||||
if a.sum(1).max() == 1 and a.sum(0).max() == 1:
|
||||
matched_indices = np.stack(np.where(a), axis=1)
|
||||
else:
|
||||
matched_indices = linear_assignment(-iou_matrix)
|
||||
else:
|
||||
matched_indices = np.empty(shape=(0,2))
|
||||
|
||||
unmatched_detections = []
|
||||
for d, det in enumerate(detections):
|
||||
if(d not in matched_indices[:,0]):
|
||||
unmatched_detections.append(d)
|
||||
unmatched_trackers = []
|
||||
for t, trk in enumerate(trackers):
|
||||
if(t not in matched_indices[:,1]):
|
||||
unmatched_trackers.append(t)
|
||||
|
||||
#filter out matched with low IOU
|
||||
matches = []
|
||||
for m in matched_indices:
|
||||
if(iou_matrix[m[0], m[1]]<iou_threshold):
|
||||
unmatched_detections.append(m[0])
|
||||
unmatched_trackers.append(m[1])
|
||||
else:
|
||||
matches.append(m.reshape(1,2))
|
||||
if(len(matches)==0):
|
||||
matches = np.empty((0,2),dtype=int)
|
||||
else:
|
||||
matches = np.concatenate(matches,axis=0)
|
||||
|
||||
return matches, np.array(unmatched_detections), np.array(unmatched_trackers)
|
||||
|
||||
|
||||
def associate(detections, trackers, iou_threshold, velocities, previous_obs, vdc_weight):
|
||||
if(len(trackers)==0):
|
||||
return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)
|
||||
|
||||
Y, X = speed_direction_batch(detections, previous_obs)
|
||||
inertia_Y, inertia_X = velocities[:,0], velocities[:,1]
|
||||
inertia_Y = np.repeat(inertia_Y[:, np.newaxis], Y.shape[1], axis=1)
|
||||
inertia_X = np.repeat(inertia_X[:, np.newaxis], X.shape[1], axis=1)
|
||||
diff_angle_cos = inertia_X * X + inertia_Y * Y
|
||||
diff_angle_cos = np.clip(diff_angle_cos, a_min=-1, a_max=1)
|
||||
diff_angle = np.arccos(diff_angle_cos)
|
||||
diff_angle = (np.pi /2.0 - np.abs(diff_angle)) / np.pi
|
||||
|
||||
valid_mask = np.ones(previous_obs.shape[0])
|
||||
valid_mask[np.where(previous_obs[:,4]<0)] = 0
|
||||
|
||||
iou_matrix = iou_batch(detections, trackers)
|
||||
scores = np.repeat(detections[:,-1][:, np.newaxis], trackers.shape[0], axis=1)
|
||||
# iou_matrix = iou_matrix * scores # a trick sometiems works, we don't encourage this
|
||||
valid_mask = np.repeat(valid_mask[:, np.newaxis], X.shape[1], axis=1)
|
||||
|
||||
angle_diff_cost = (valid_mask * diff_angle) * vdc_weight
|
||||
angle_diff_cost = angle_diff_cost.T
|
||||
angle_diff_cost = angle_diff_cost * scores
|
||||
|
||||
if min(iou_matrix.shape) > 0:
|
||||
a = (iou_matrix > iou_threshold).astype(np.int32)
|
||||
if a.sum(1).max() == 1 and a.sum(0).max() == 1:
|
||||
matched_indices = np.stack(np.where(a), axis=1)
|
||||
else:
|
||||
matched_indices = linear_assignment(-(iou_matrix+angle_diff_cost))
|
||||
else:
|
||||
matched_indices = np.empty(shape=(0,2))
|
||||
|
||||
unmatched_detections = []
|
||||
for d, det in enumerate(detections):
|
||||
if(d not in matched_indices[:,0]):
|
||||
unmatched_detections.append(d)
|
||||
unmatched_trackers = []
|
||||
for t, trk in enumerate(trackers):
|
||||
if(t not in matched_indices[:,1]):
|
||||
unmatched_trackers.append(t)
|
||||
|
||||
# filter out matched with low IOU
|
||||
matches = []
|
||||
for m in matched_indices:
|
||||
if(iou_matrix[m[0], m[1]]<iou_threshold):
|
||||
unmatched_detections.append(m[0])
|
||||
unmatched_trackers.append(m[1])
|
||||
else:
|
||||
matches.append(m.reshape(1,2))
|
||||
if(len(matches)==0):
|
||||
matches = np.empty((0,2),dtype=int)
|
||||
else:
|
||||
matches = np.concatenate(matches,axis=0)
|
||||
|
||||
return matches, np.array(unmatched_detections), np.array(unmatched_trackers)
|
||||
|
||||
|
||||
def associate_kitti(detections, trackers, det_cates, iou_threshold,
|
||||
velocities, previous_obs, vdc_weight):
|
||||
if(len(trackers)==0):
|
||||
return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)
|
||||
|
||||
"""
|
||||
Cost from the velocity direction consistency
|
||||
"""
|
||||
Y, X = speed_direction_batch(detections, previous_obs)
|
||||
inertia_Y, inertia_X = velocities[:,0], velocities[:,1]
|
||||
inertia_Y = np.repeat(inertia_Y[:, np.newaxis], Y.shape[1], axis=1)
|
||||
inertia_X = np.repeat(inertia_X[:, np.newaxis], X.shape[1], axis=1)
|
||||
diff_angle_cos = inertia_X * X + inertia_Y * Y
|
||||
diff_angle_cos = np.clip(diff_angle_cos, a_min=-1, a_max=1)
|
||||
diff_angle = np.arccos(diff_angle_cos)
|
||||
diff_angle = (np.pi /2.0 - np.abs(diff_angle)) / np.pi
|
||||
|
||||
valid_mask = np.ones(previous_obs.shape[0])
|
||||
valid_mask[np.where(previous_obs[:,4]<0)]=0
|
||||
valid_mask = np.repeat(valid_mask[:, np.newaxis], X.shape[1], axis=1)
|
||||
|
||||
scores = np.repeat(detections[:,-1][:, np.newaxis], trackers.shape[0], axis=1)
|
||||
angle_diff_cost = (valid_mask * diff_angle) * vdc_weight
|
||||
angle_diff_cost = angle_diff_cost.T
|
||||
angle_diff_cost = angle_diff_cost * scores
|
||||
|
||||
"""
|
||||
Cost from IoU
|
||||
"""
|
||||
iou_matrix = iou_batch(detections, trackers)
|
||||
|
||||
|
||||
"""
|
||||
With multiple categories, generate the cost for catgory mismatch
|
||||
"""
|
||||
num_dets = detections.shape[0]
|
||||
num_trk = trackers.shape[0]
|
||||
cate_matrix = np.zeros((num_dets, num_trk))
|
||||
for i in range(num_dets):
|
||||
for j in range(num_trk):
|
||||
if det_cates[i] != trackers[j, 4]:
|
||||
cate_matrix[i][j] = -1e6
|
||||
|
||||
cost_matrix = - iou_matrix -angle_diff_cost - cate_matrix
|
||||
|
||||
if min(iou_matrix.shape) > 0:
|
||||
a = (iou_matrix > iou_threshold).astype(np.int32)
|
||||
if a.sum(1).max() == 1 and a.sum(0).max() == 1:
|
||||
matched_indices = np.stack(np.where(a), axis=1)
|
||||
else:
|
||||
matched_indices = linear_assignment(cost_matrix)
|
||||
else:
|
||||
matched_indices = np.empty(shape=(0,2))
|
||||
|
||||
unmatched_detections = []
|
||||
for d, det in enumerate(detections):
|
||||
if(d not in matched_indices[:,0]):
|
||||
unmatched_detections.append(d)
|
||||
unmatched_trackers = []
|
||||
for t, trk in enumerate(trackers):
|
||||
if(t not in matched_indices[:,1]):
|
||||
unmatched_trackers.append(t)
|
||||
|
||||
#filter out matched with low IOU
|
||||
matches = []
|
||||
for m in matched_indices:
|
||||
if(iou_matrix[m[0], m[1]]<iou_threshold):
|
||||
unmatched_detections.append(m[0])
|
||||
unmatched_trackers.append(m[1])
|
||||
else:
|
||||
matches.append(m.reshape(1,2))
|
||||
if(len(matches)==0):
|
||||
matches = np.empty((0,2),dtype=int)
|
||||
else:
|
||||
matches = np.concatenate(matches,axis=0)
|
||||
|
||||
return matches, np.array(unmatched_detections), np.array(unmatched_trackers)
|
12
feeder/trackers/ocsort/configs/ocsort.yaml
Normal file
12
feeder/trackers/ocsort/configs/ocsort.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Trial number: 137
|
||||
# HOTA, MOTA, IDF1: [55.567]
|
||||
ocsort:
|
||||
asso_func: giou
|
||||
conf_thres: 0.5122620708221085
|
||||
delta_t: 1
|
||||
det_thresh: 0
|
||||
inertia: 0.3941737016672115
|
||||
iou_thresh: 0.22136877277096445
|
||||
max_age: 50
|
||||
min_hits: 1
|
||||
use_byte: false
|
1581
feeder/trackers/ocsort/kalmanfilter.py
Normal file
1581
feeder/trackers/ocsort/kalmanfilter.py
Normal file
File diff suppressed because it is too large
Load diff
328
feeder/trackers/ocsort/ocsort.py
Normal file
328
feeder/trackers/ocsort/ocsort.py
Normal file
|
@ -0,0 +1,328 @@
|
|||
"""
|
||||
This script is adopted from the SORT script by Alex Bewley alex@bewley.ai
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import numpy as np
|
||||
from .association import *
|
||||
from ultralytics.yolo.utils.ops import xywh2xyxy
|
||||
|
||||
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
count = 0
|
||||
|
||||
def __init__(self, bbox, cls, delta_t=3, orig=False):
|
||||
"""
|
||||
Initialises a tracker using initial bounding box.
|
||||
|
||||
"""
|
||||
# define constant velocity model
|
||||
if not orig:
|
||||
from .kalmanfilter import KalmanFilterNew as KalmanFilter
|
||||
self.kf = KalmanFilter(dim_x=7, dim_z=4)
|
||||
else:
|
||||
from filterpy.kalman import KalmanFilter
|
||||
self.kf = KalmanFilter(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.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
|
||||
self.conf = bbox[-1]
|
||||
self.cls = cls
|
||||
"""
|
||||
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, cls):
|
||||
"""
|
||||
Updates the state vector with observed bbox.
|
||||
"""
|
||||
|
||||
if bbox is not None:
|
||||
self.conf = bbox[-1]
|
||||
self.cls = cls
|
||||
if 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))
|
||||
return self.history[-1]
|
||||
|
||||
def get_state(self):
|
||||
"""
|
||||
Returns the current bounding box estimate.
|
||||
"""
|
||||
return convert_x_to_bbox(self.kf.x)
|
||||
|
||||
|
||||
"""
|
||||
We support multiple ways for association cost calculation, by default
|
||||
we use IoU. GIoU may have better performance in some situations. We note
|
||||
that we hardly normalize the cost by all methods to (0,1) which may not be
|
||||
the best practice.
|
||||
"""
|
||||
ASSO_FUNCS = { "iou": iou_batch,
|
||||
"giou": giou_batch,
|
||||
"ciou": ciou_batch,
|
||||
"diou": diou_batch,
|
||||
"ct_dist": ct_dist}
|
||||
|
||||
|
||||
class OCSort(object):
|
||||
def __init__(self, det_thresh, max_age=30, min_hits=3,
|
||||
iou_threshold=0.3, delta_t=3, asso_func="iou", inertia=0.2, use_byte=False):
|
||||
"""
|
||||
Sets key parameters for SORT
|
||||
"""
|
||||
self.max_age = max_age
|
||||
self.min_hits = min_hits
|
||||
self.iou_threshold = iou_threshold
|
||||
self.trackers = []
|
||||
self.frame_count = 0
|
||||
self.det_thresh = det_thresh
|
||||
self.delta_t = delta_t
|
||||
self.asso_func = ASSO_FUNCS[asso_func]
|
||||
self.inertia = inertia
|
||||
self.use_byte = use_byte
|
||||
KalmanBoxTracker.count = 0
|
||||
|
||||
def update(self, dets, _):
|
||||
"""
|
||||
Params:
|
||||
dets - a numpy array of detections in the format [[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]
|
||||
Requires: this method must be called once for each frame even with empty detections (use np.empty((0, 5)) for frames without detections).
|
||||
Returns the a similar array, where the last column is the object ID.
|
||||
NOTE: The number of objects returned may differ from the number of detections provided.
|
||||
"""
|
||||
|
||||
self.frame_count += 1
|
||||
|
||||
xyxys = dets[:, 0:4]
|
||||
confs = dets[:, 4]
|
||||
clss = dets[:, 5]
|
||||
|
||||
classes = clss.numpy()
|
||||
xyxys = xyxys.numpy()
|
||||
confs = confs.numpy()
|
||||
|
||||
output_results = np.column_stack((xyxys, confs, classes))
|
||||
|
||||
inds_low = confs > 0.1
|
||||
inds_high = confs < self.det_thresh
|
||||
inds_second = np.logical_and(inds_low, inds_high) # self.det_thresh > score > 0.1, for second matching
|
||||
dets_second = output_results[inds_second] # detections for second matching
|
||||
remain_inds = confs > self.det_thresh
|
||||
dets = output_results[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)
|
||||
|
||||
velocities = np.array(
|
||||
[trk.velocity if trk.velocity is not None else np.array((0, 0)) for trk in self.trackers])
|
||||
last_boxes = np.array([trk.last_observation for trk in self.trackers])
|
||||
k_observations = np.array(
|
||||
[k_previous_obs(trk.observations, trk.age, self.delta_t) for trk in self.trackers])
|
||||
|
||||
"""
|
||||
First round of association
|
||||
"""
|
||||
matched, unmatched_dets, unmatched_trks = associate(
|
||||
dets, trks, self.iou_threshold, velocities, k_observations, self.inertia)
|
||||
for m in matched:
|
||||
self.trackers[m[1]].update(dets[m[0], :5], dets[m[0], 5])
|
||||
|
||||
"""
|
||||
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 = self.asso_func(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, :5], dets_second[det_ind, 5])
|
||||
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 = self.asso_func(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, :5], dets[det_ind, 5])
|
||||
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, None)
|
||||
|
||||
# create and initialise new trackers for unmatched detections
|
||||
for i in unmatched_dets:
|
||||
trk = KalmanBoxTracker(dets[i, :5], dets[i, 5], 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:
|
||||
"""
|
||||
this is optional to use the recent observation or the kalman filter prediction,
|
||||
we didn't notice significant difference here
|
||||
"""
|
||||
d = trk.last_observation[:4]
|
||||
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], [trk.cls], [trk.conf])).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, 5))
|
Loading…
Add table
Add a link
Reference in a new issue