- 상세 설명: - 군중 위험 감지 로직 추가 (거리 기반·속도 기반 위험도 산출 및 Optical Flow 적용) - OC-SORT 트래킹 알고리즘 연동 - 얼굴 인식, 이상 행동 감지, 차량 번호판 인식 결과를 card 형태의 메타데이터로 구성 - 테스트 사항: - Node.js 연동을 통한 실시간 시각화 테스트 완료main
parent
a0e204e3d3
commit
aa853c4f26
@ -0,0 +1,71 @@
|
||||
from collections import defaultdict, deque
|
||||
|
||||
|
||||
def distance_risk(items, thr=80):
|
||||
centers = [((it["x1"]+it["x2"])/2, (it["y1"]+it["y2"])/2) for it in items]
|
||||
n = len(centers)
|
||||
if n < 2:
|
||||
return 0.0
|
||||
|
||||
close_pairs = 0
|
||||
for i in range(n):
|
||||
cx1, cy1 = centers[i]
|
||||
for j in range(i+1, n):
|
||||
cx2, cy2 = centers[j]
|
||||
dist = ((cx1-cx2)**2 + (cy1-cy2)**2)**0.5
|
||||
if dist < thr:
|
||||
close_pairs += 1
|
||||
|
||||
return min(close_pairs / 30.0, 1.0)
|
||||
|
||||
|
||||
prev_centers = {}
|
||||
trajectories = defaultdict(lambda: deque(maxlen=10))
|
||||
last_seen = {}
|
||||
KEEP_FRAMES = 15
|
||||
frame_counter = 0
|
||||
|
||||
|
||||
def motion_risk_and_path(items):
|
||||
global prev_centers, trajectories, last_seen, frame_counter
|
||||
|
||||
frame_counter += 1
|
||||
fid = frame_counter
|
||||
|
||||
curr = {}
|
||||
speeds = []
|
||||
|
||||
for it in items:
|
||||
|
||||
if it["tid"] == 65535:
|
||||
continue
|
||||
|
||||
tid = it["tid"]
|
||||
|
||||
cx = (it["x1"] + it["x2"]) / 2
|
||||
cy = (it["y1"] + it["y2"]) / 2
|
||||
|
||||
curr[tid] = (cx, cy)
|
||||
trajectories[tid].append((cx, cy))
|
||||
|
||||
last_seen[tid] = fid
|
||||
|
||||
if tid in prev_centers:
|
||||
px, py = prev_centers[tid]
|
||||
spd = ((cx - px)**2 + (cy - py)**2)**0.5
|
||||
speeds.append(spd)
|
||||
|
||||
prev_centers = curr
|
||||
|
||||
for tid in list(trajectories.keys()):
|
||||
if tid not in curr:
|
||||
if fid - last_seen.get(tid, fid) > KEEP_FRAMES:
|
||||
del trajectories[tid]
|
||||
del last_seen[tid]
|
||||
|
||||
if not speeds:
|
||||
risk = 0.0
|
||||
else:
|
||||
risk = min((sum(speeds) / len(speeds)) / 20.0, 1.0)
|
||||
|
||||
return risk, trajectories
|
||||
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CLI="python3 $BASE_DIR/ctrl_cli.py"
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 {IN|OUT|SET value|STATUS}" >&2
|
||||
echo " 예) $0 IN"
|
||||
echo " 예) $0 OUT"
|
||||
echo " 예) $0 SET 3"
|
||||
echo " 예) $0 STATUS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CMD="$1"
|
||||
|
||||
case "$CMD" in
|
||||
IN)
|
||||
$CLI "ZOOM_IN"
|
||||
;;
|
||||
OUT)
|
||||
$CLI "ZOOM_OUT"
|
||||
;;
|
||||
*)
|
||||
echo "ERR: unknown command '$CMD'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,20 @@
|
||||
; config file for yolov8s_coco_640x384_apache6sr250_aiw4939.aiwbin
|
||||
; author:
|
||||
; date:
|
||||
; network input size:
|
||||
|
||||
[detection] ; network
|
||||
|
||||
class_num = 12 ; number of class
|
||||
class_name = shirt, t-shirt, sweater, cardigan, jacket, vest, pants, shorts, skirt, coat, dress, bag
|
||||
|
||||
class_color = {255, 255, 255},{0, 0, 255},{255, 0, 0},{0, 255, 255},{55, 255, 0},{0, 255, 0},{255, 255, 255},{0, 0, 255},{255, 0, 0},{0, 255, 255},{55, 255, 0},{0, 255, 0}
|
||||
|
||||
CONFIDENCE_DETECTION_THRESHOLD = 0.4 ; detection threshold
|
||||
CONFIDENCE_NMS_THRESHOLD = 0.3 ; nms threshold
|
||||
|
||||
; caution
|
||||
; 1. Write the value of each key on one line
|
||||
; 2. class_num = number of class_name = number of class_color
|
||||
; 3. range of threshold -> 0 < threshold < 1
|
||||
; 4. Class IDs are determined in the order of class_name. (0,1,2,3 ...)
|
||||
Binary file not shown.
@ -0,0 +1,379 @@
|
||||
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
|
||||
union = ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
iou = wh / union
|
||||
|
||||
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 - union) / 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
|
||||
union = ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
iou = wh / union
|
||||
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
|
||||
union = ((bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1])
|
||||
+ (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) - wh)
|
||||
iou = wh / union
|
||||
|
||||
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)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,431 @@
|
||||
"""
|
||||
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 *
|
||||
|
||||
|
||||
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, 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
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Updates the state vector with observed bbox.
|
||||
"""
|
||||
if bbox is not None:
|
||||
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, output_results, img_info, img_size):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if output_results is None:
|
||||
return np.empty((0, 5))
|
||||
|
||||
self.frame_count += 1
|
||||
# post_process detections
|
||||
if output_results.shape[1] == 5:
|
||||
scores = output_results[:, 4]
|
||||
bboxes = output_results[:, :4]
|
||||
else:
|
||||
output_results = output_results.cpu().numpy()
|
||||
scores = output_results[:, 4] * output_results[:, 5]
|
||||
bboxes = output_results[:, :4] # x1y1x2y2
|
||||
img_h, img_w = img_info[0], img_info[1]
|
||||
scale = min(img_size[0] / float(img_h), img_size[1] / float(img_w))
|
||||
bboxes /= scale
|
||||
dets = np.concatenate((bboxes, np.expand_dims(scores, axis=-1)), axis=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)
|
||||
|
||||
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], :])
|
||||
|
||||
"""
|
||||
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, :])
|
||||
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, :])
|
||||
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:
|
||||
"""
|
||||
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])).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))
|
||||
|
||||
def update_public(self, dets, cates, scores):
|
||||
self.frame_count += 1
|
||||
|
||||
det_scores = np.ones((dets.shape[0], 1))
|
||||
dets = np.concatenate((dets, det_scores), axis=1)
|
||||
|
||||
remain_inds = scores > self.det_thresh
|
||||
|
||||
cates = cates[remain_inds]
|
||||
dets = dets[remain_inds]
|
||||
|
||||
trks = np.zeros((len(self.trackers), 5))
|
||||
to_del = []
|
||||
ret = []
|
||||
for t, trk in enumerate(trks):
|
||||
pos = self.trackers[t].predict()[0]
|
||||
cat = self.trackers[t].cate
|
||||
trk[:] = [pos[0], pos[1], pos[2], pos[3], cat]
|
||||
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])
|
||||
|
||||
matched, unmatched_dets, unmatched_trks = associate_kitti\
|
||||
(dets, trks, cates, self.iou_threshold, velocities, k_observations, self.inertia)
|
||||
|
||||
for m in matched:
|
||||
self.trackers[m[1]].update(dets[m[0], :])
|
||||
|
||||
if unmatched_dets.shape[0] > 0 and unmatched_trks.shape[0] > 0:
|
||||
"""
|
||||
The re-association stage by OCR.
|
||||
NOTE: at this stage, adding other strategy might be able to continue improve
|
||||
the performance, such as BYTE association by ByteTrack.
|
||||
"""
|
||||
left_dets = dets[unmatched_dets]
|
||||
left_trks = last_boxes[unmatched_trks]
|
||||
left_dets_c = left_dets.copy()
|
||||
left_trks_c = left_trks.copy()
|
||||
|
||||
iou_left = self.asso_func(left_dets_c, left_trks_c)
|
||||
iou_left = np.array(iou_left)
|
||||
det_cates_left = cates[unmatched_dets]
|
||||
trk_cates_left = trks[unmatched_trks][:,4]
|
||||
num_dets = unmatched_dets.shape[0]
|
||||
num_trks = unmatched_trks.shape[0]
|
||||
cate_matrix = np.zeros((num_dets, num_trks))
|
||||
for i in range(num_dets):
|
||||
for j in range(num_trks):
|
||||
if det_cates_left[i] != trk_cates_left[j]:
|
||||
"""
|
||||
For some datasets, such as KITTI, there are different categories,
|
||||
we have to avoid associate them together.
|
||||
"""
|
||||
cate_matrix[i][j] = -1e6
|
||||
iou_left = iou_left + cate_matrix
|
||||
if iou_left.max() > self.iou_threshold - 0.1:
|
||||
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 - 0.1:
|
||||
continue
|
||||
self.trackers[trk_ind].update(dets[det_ind, :])
|
||||
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 i in unmatched_dets:
|
||||
trk = KalmanBoxTracker(dets[i,:])
|
||||
trk.cate = cates[i]
|
||||
self.trackers.append(trk)
|
||||
i = len(self.trackers)
|
||||
|
||||
for trk in reversed(self.trackers):
|
||||
if trk.last_observation.sum() > 0:
|
||||
d = trk.last_observation[:4]
|
||||
else:
|
||||
d = trk.get_state()[0]
|
||||
if (trk.time_since_update < 1):
|
||||
if (self.frame_count <= self.min_hits) or (trk.hit_streak >= self.min_hits):
|
||||
# id+1 as MOT benchmark requires positive
|
||||
ret.append(np.concatenate((d, [trk.id+1], [trk.cate], [0])).reshape(1,-1))
|
||||
if trk.hit_streak == self.min_hits:
|
||||
# Head Padding (HP): recover the lost steps during initializing the track
|
||||
for prev_i in range(self.min_hits - 1):
|
||||
prev_observation = trk.history_observations[-(prev_i+2)]
|
||||
ret.append((np.concatenate((prev_observation[:4], [trk.id+1], [trk.cate],
|
||||
[-(prev_i+1)]))).reshape(1,-1))
|
||||
i -= 1
|
||||
if (trk.time_since_update > self.max_age):
|
||||
self.trackers.pop(i)
|
||||
|
||||
if(len(ret)>0):
|
||||
return np.concatenate(ret)
|
||||
return np.empty((0, 7))
|
||||
|
||||
|
||||
Loading…
Reference in new issue