feat: 군중 위험 감지 및 객체 추적·메타데이터 카드 기능 추가

- 상세 설명:
  - 군중 위험 감지 로직 추가 (거리 기반·속도 기반 위험도 산출 및 Optical Flow 적용)
  - OC-SORT 트래킹 알고리즘 연동
  - 얼굴 인식, 이상 행동 감지, 차량 번호판 인식 결과를 card 형태의 메타데이터로 구성

- 테스트 사항:
  - Node.js 연동을 통한 실시간 시각화 테스트 완료
main
송현수 6 months ago
parent a0e204e3d3
commit aa853c4f26

@ -108,8 +108,6 @@ C 스트리머 기능(OBJDET, FIRE, FACE 등)을 ON/OFF 할 수 있습니다.
feat_control/
├── ctrl_cli.py # 기능 제어 메시지를 UDS로 전송하는 Python CLI
└── feat_on.sh # 여러 기능을 한 번에 ON/OFF 제어하는 스크립트
```
---

@ -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 ...)

BIN
trackers/.DS_Store vendored

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))

@ -1,9 +1,20 @@
import socket, struct, sys, json, asyncio, threading
import websockets
import time
import numpy as np
from threading import Lock
from collections import deque
from crowd_risk import distance_risk, motion_risk_and_path
from trackers.ocsort_tracker.ocsort import OCSort
import time
import cv2, base64
crowd_tracker = OCSort(det_thresh=0.3, iou_threshold=0.3, max_age=30, min_hits=1, delta_t=1)
face_tracker = OCSort(det_thresh=0.3, iou_threshold=0.3, max_age=30, min_hits=1, delta_t=1)
car_tracker = OCSort(det_thresh=0.3, iou_threshold=0.3, max_age=30, min_hits=1, delta_t=1)
anomy_tracker = OCSort(det_thresh=0.3, iou_threshold=0.3, max_age=30, min_hits=1, delta_t=1)
vip_tracker = OCSort(det_thresh=0.3, iou_threshold=0.3, max_age=30, min_hits=1, delta_t=1)
encode_queue = deque()
encode_lock = Lock()
@ -11,6 +22,10 @@ encode_lock = Lock()
MAX_ENC_QUEUE = 4
frames_buffer = {}
id_card_state = {}
CARD_SEND_INTERVAL = 1.0
CARD_TIMEOUT = 0.0
SOCK_PATH = sys.argv[1] if len(sys.argv) > 1 else "/tmp/cam.sock"
@ -33,6 +48,7 @@ UDS_TAG_DET = 1
UDS_TAG_FIRE = 2
UDS_TAG_FACE = 3
UDS_TAG_LPR = 4
UDS_TAG_ABNORMAL = 5
FEAT_OBJDET = 0
FEAT_ABNORM = 1
@ -42,10 +58,7 @@ FEAT_LPR = 4
FEAT_FACEATTR = 5
FEAT_VIPTRACK = 6
CROWD_LOCAL_CLS = 3
# H = 640
# W = 384
pre_feat_idx = None
W = 1080
H = 720
@ -115,6 +128,13 @@ async def ws_broadcast_text(text: str):
if not ws_clients:
return
try:
meta = json.loads(text)
if meta.get("type") == "card":
print(f"[WS][TEXT] send card -> clients={len(ws_clients)}")
except Exception:
pass
targets = list(ws_clients)
coros = [ws.send(text) for ws in targets]
results = await asyncio.gather(*coros, return_exceptions=True)
@ -184,6 +204,75 @@ def decode_reserved(resv):
return fm8, md8
def fm8_to_feature_index(fm8):
if fm8 is None:
return None
if fm8 == 0:
return None
return (fm8 & -fm8).bit_length() - 1
def iou(a, b):
ax1, ay1, ax2, ay2 = a
bx1, by1, bx2, by2 = b
inter_x1 = max(ax1, bx1)
inter_y1 = max(ay1, by1)
inter_x2 = min(ax2, bx2)
inter_y2 = min(ay2, by2)
inter_w = max(0, inter_x2 - inter_x1)
inter_h = max(0, inter_y2 - inter_y1)
inter_area = inter_w * inter_h
area_a = (ax2 - ax1) * (ay2 - ay1)
area_b = (bx2 - bx1) * (by2 - by1)
union_area = area_a + area_b - inter_area
if union_area <= 0:
return 0.0
return inter_area / union_area
def sanitize_dets_xyxy_score(
dets_xyxy_score: np.ndarray, W: int, H: int, min_wh: float = 1.0
) -> np.ndarray:
"""
dets_xyxy_score: (N,5) = [x1,y1,x2,y2,score]
"""
if dets_xyxy_score is None or len(dets_xyxy_score) == 0:
return np.empty((0, 5), dtype=np.float32)
d = dets_xyxy_score.astype(np.float32, copy=True)
finite_mask = np.all(np.isfinite(d[:, :5]), axis=1)
d = d[finite_mask]
if d.shape[0] == 0:
return np.empty((0, 5), dtype=np.float32)
d[:, [0, 2]] = np.clip(d[:, [0, 2]], 0, W - 1)
d[:, [1, 3]] = np.clip(d[:, [1, 3]], 0, H - 1)
x1, y1, x2, y2 = d[:, 0], d[:, 1], d[:, 2], d[:, 3]
d[:, 0], d[:, 1], d[:, 2], d[:, 3] = (
np.minimum(x1, x2),
np.minimum(y1, y2),
np.maximum(x1, x2),
np.maximum(y1, y2),
)
w = d[:, 2] - d[:, 0]
h = d[:, 3] - d[:, 1]
size_mask = (w >= min_wh) & (h >= min_wh)
d = d[size_mask]
if d.shape[0] > 0:
d = d[d[:, 4] > 0.0]
return d
def handle_frame(hdr, sock):
magic, ver, ch, w, h, stride, pixfmt, bytes_len, ts_us = hdr
@ -215,8 +304,21 @@ def handle_det(hdr, sock):
entries_raw = read_exact(sock, need_bytes) if count > 0 else b""
items = []
det_rows = []
det_face = []
det_car = []
off = 0
if count > 0:
_, _, _, _, _, _, _, first_resv = struct.unpack_from(
ENTRY_FMT, entries_raw, 0)
fm8, md8 = decode_reserved(first_resv)
feat_idx = fm8_to_feature_index(fm8)
else:
fm8, md8 = None, None
feat_idx = None
for _ in range(count):
prob, x, y, w, h, cls, tid, resv = struct.unpack_from(
ENTRY_FMT, entries_raw, off)
@ -227,29 +329,218 @@ def handle_det(hdr, sock):
tag = uds_dec_tag(cls)
local_cls = uds_dec_local(cls)
if tag == UDS_TAG_DET and local_cls == 0 and feat_idx == FEAT_CROWD:
det_rows.append([x1, y1, x2, y2, prob, int(local_cls)])
elif tag == UDS_TAG_DET and local_cls == 0 and feat_idx == FEAT_FACEATTR:
det_rows.append([x1, y1, x2, y2, prob, int(local_cls)])
elif tag == UDS_TAG_LPR and local_cls == 0 and feat_idx == FEAT_LPR:
det_rows.append([x1, y1, x2, y2, prob, int(local_cls)])
elif tag == UDS_TAG_ABNORMAL and local_cls == 0 and feat_idx == FEAT_ABNORM:
det_rows.append([x1, y1, x2, y2, prob, int(local_cls)])
elif tag == UDS_TAG_DET and local_cls == 0 and feat_idx == FEAT_VIPTRACK:
det_rows.append([x1, y1, x2, y2, prob, int(local_cls)])
else:
if feat_idx == FEAT_FACEATTR and tag == UDS_TAG_FACE:
det_face.append([x1, y1, x2, y2])
if feat_idx == FEAT_LPR and tag == UDS_TAG_DET and local_cls == 2:
det_car.append([x1, y1, x2, y2])
items.append({
"x1": float(x1),
"y1": float(y1),
"x2": float(x2),
"y2": float(y2),
"score": float(prob),
"cls": int(local_cls),
"tag": int(tag),
"tid": int(tid)
})
if det_rows:
arr = np.asarray(det_rows, dtype=np.float32)
dets_xyxy_score = arr[:, :5]
track_cls = 0
tag = int(UDS_TAG_DET)
dets_for_tracker = sanitize_dets_xyxy_score(dets_xyxy_score, W, H, min_wh=1.0)
if feat_idx == FEAT_CROWD:
tracks = crowd_tracker.update(dets_for_tracker, img_info=(H, W), img_size=(H, W))
if feat_idx == FEAT_FACEATTR:
tracks = face_tracker.update(dets_for_tracker, img_info=(H, W), img_size=(H, W))
if feat_idx == FEAT_LPR:
tracks = car_tracker.update(dets_for_tracker, img_info=(H, W), img_size=(H, W))
tag = UDS_TAG_LPR
if feat_idx == FEAT_ABNORM:
tracks = anomy_tracker.update(dets_for_tracker, img_info=(H, W), img_size=(H, W))
tag = UDS_TAG_ABNORMAL
if feat_idx == FEAT_VIPTRACK:
tracks = vip_tracker.update(dets_for_tracker, img_info=(H, W), img_size=(H, W))
if tracks is not None and len(tracks) > 0:
for t in tracks:
if t.shape[0] < 5:
continue
tx1, ty1, tx2, ty2, tid = float(t[0]), float(t[1]), float(t[2]), float(t[3]), int(t[4])
items.append({
"x1": tx1, "y1": ty1,
"x2": tx2, "y2": ty2,
"tid": tid,
"cls": track_cls,
"tag": tag,
})
if not frames_buffer:
return
candidate_ts = min(frames_buffer.keys(), key=lambda t: abs(t - ts_us))
if abs(candidate_ts - ts_us) > 200000:
now_fail = time.time()
for it in items:
tid2 = it["tid"]
st = id_card_state.get(tid2)
if not st:
continue
if now_fail - st["last_update"] > CARD_TIMEOUT:
id_card_state.pop(tid2, None)
return
jpeg_bytes = frames_buffer.pop(candidate_ts, None)
if jpeg_bytes is None:
return
now = time.time()
if feat_idx == FEAT_FACEATTR:
for it in items:
if it["tag"] != UDS_TAG_DET:
continue
if it["cls"] != 0:
continue
tid2 = it["tid"]
x1 = it["x1"]
y1 = it["y1"]
x2 = it["x2"]
y2 = it["y2"]
id_card_state["frame"] = jpeg_bytes
id_card_state["feat_idx"] = feat_idx
id_card_state[tid2] = {
"bbox": [x1, y1, x2, y2],
"last_update": now,
}
best_face = None
best_iou = 0.0
person_bbox = [x1, y1, x2, y2]
for fx1, fy1, fx2, fy2 in det_face:
face_bbox = [fx1, fy1, fx2, fy2]
i = iou(person_bbox, face_bbox)
if i > best_iou:
best_iou = i
best_face = face_bbox
if best_face is not None:
id_card_state[tid2]["face"] = best_face
if feat_idx == FEAT_ABNORM:
for it in items:
if it["tag"] != UDS_TAG_ABNORMAL:
continue
if it["cls"] != 0:
continue
tid2 = it["tid"]
x1 = it["x1"]
y1 = it["y1"]
x2 = it["x2"]
y2 = it["y2"]
id_card_state["frame"] = jpeg_bytes
id_card_state["feat_idx"] = feat_idx
id_card_state[tid2] = {
"bbox": [x1, y1, x2, y2],
"last_update": now,
}
if feat_idx == FEAT_LPR:
for it in items:
if it["tag"] != UDS_TAG_LPR:
continue
if it["cls"] != 0:
continue
tid2 = it["tid"]
x1 = it["x1"]
y1 = it["y1"]
x2 = it["x2"]
y2 = it["y2"]
id_card_state["frame"] = jpeg_bytes
id_card_state["feat_idx"] = feat_idx
id_card_state[tid2] = {
"bbox": [x1, y1, x2, y2],
"last_update": now,
}
best_car = None
best_iou = 0.0
lp_bbox = [x1, y1, x2, y2]
for fx1, fy1, fx2, fy2 in det_car:
print("det_car:", fx1, fy1, fx2, fy2)
car_bbox = [fx1, fy1, fx2, fy2]
i = iou(lp_bbox, car_bbox)
if i > best_iou:
best_iou = i
best_car = car_bbox
if best_car is not None:
print("best_car:", best_car)
id_card_state[tid2]["car"] = best_car
if feat_idx == FEAT_VIPTRACK:
persons = []
for it in items:
if it.get("tag") == UDS_TAG_DET and it.get("cls") == 0:
persons.append(it)
if persons:
for it in items:
if it.get("tag") != UDS_TAG_FACE:
continue
face_bbox = [it["x1"], it["y1"], it["x2"], it["y2"]]
best_iou = 0.0
best_person_tid = None
for p in persons:
person_bbox = [p["x1"], p["y1"], p["x2"], p["y2"]]
i = iou(person_bbox, face_bbox)
if i > best_iou:
best_iou = i
best_person_tid = p["tid"]
if best_person_tid is not None:
it["tid"] = int(best_person_tid)
meta = {
"type": "frame",
"ch": int(ch),
@ -259,6 +550,24 @@ def handle_det(hdr, sock):
"items": items,
}
if feat_idx == FEAT_CROWD:
dist_val = distance_risk(items)
motion_val, traj = motion_risk_and_path(items)
risk_info = {
"dist_risk": dist_val,
"motion_risk": motion_val,
"risk_total": max(dist_val, motion_val)
}
trajectory = traj
if risk_info is not None:
meta["risk"] = risk_info
if trajectory is not None:
meta["trajectory"] = {tid: list(path) for tid, path in trajectory.items()}
def push():
if len(frame_queue) >= 5:
frame_queue.popleft()
@ -336,6 +645,8 @@ def uds_loop():
time.sleep(1)
import asyncio
async def ws_frame_sender():
global ws_frames_total, ws_frames_sec
@ -345,26 +656,144 @@ async def ws_frame_sender():
await asyncio.sleep(0.005)
continue
# deque.pop() 빈 경우는 IndexError가 일반적
try:
meta, jpeg_bytes = frame_queue.pop()
except ValueError:
except IndexError:
await asyncio.sleep(0.002)
continue
text = json.dumps(meta)
await ws_broadcast_text(text)
await ws_broadcast_binary(jpeg_bytes)
await asyncio.wait_for(ws_broadcast_text(text), timeout=0.3)
await asyncio.wait_for(ws_broadcast_binary(jpeg_bytes), timeout=0.3)
with ws_frames_lock:
ws_frames_total += 1
ws_frames_sec += 1
except asyncio.TimeoutError:
print("[WS][FRAME_SENDER][TIMEOUT] broadcast stalled (slow client?)")
await asyncio.sleep(0.05)
except Exception as e:
print(f"[WS][FRAME_SENDER][ERR] {e}")
print(f"[WS][FRAME_SENDER][ERR] {type(e).__name__}: {e}")
await asyncio.sleep(0.05)
await asyncio.sleep(0.001)
def card_sender_loop():
while True:
global pre_feat_idx
now = time.time()
send_list = []
remove = []
frame_bytes = id_card_state.get("frame")
feat_idx = id_card_state.get("feat_idx")
if pre_feat_idx != feat_idx:
pre_feat_idx = feat_idx
continue
for tid, info in list(id_card_state.items()):
if not isinstance(tid, int):
continue
if now - info["last_update"] > CARD_TIMEOUT:
remove.append(tid)
for tid in remove:
id_card_state.pop(tid, None)
if frame_bytes is None:
time.sleep(0.1)
continue
try:
np_frame = np.frombuffer(frame_bytes, dtype=np.uint8)
frame = cv2.imdecode(np_frame, cv2.IMREAD_COLOR)
except:
time.sleep(0.1)
continue
for tid, info in list(id_card_state.items()):
if not isinstance(tid, int):
continue
face_b64 = None
car_b64 = None
x1, y1, x2, y2 = map(int, info["bbox"])
crop = frame[y1:y2, x1:x2]
if crop.size == 0:
continue
ok, buf = cv2.imencode(".jpg", crop, [cv2.IMWRITE_JPEG_QUALITY, 80])
if not ok:
continue
b64 = base64.b64encode(buf).decode("utf-8")
if feat_idx == FEAT_FACEATTR:
if "face" in info:
fx1, fy1, fx2, fy2 = map(int, info["face"])
face_crop = frame[fy1:fy2, fx1:fx2]
if face_crop.size != 0:
ok2, buf2 = cv2.imencode(".jpg", face_crop, [cv2.IMWRITE_JPEG_QUALITY, 80])
if ok2:
face_b64 = base64.b64encode(buf2).decode("utf-8")
entry = {
"tid": tid,
"person": b64,
"appear": "jacket, pants, bag"
}
if face_b64 is not None:
entry["face"] = face_b64
if feat_idx == FEAT_LPR:
if "car" in info:
fx1, fy1, fx2, fy2 = map(int, info["car"])
lp_crop = frame[fy1:fy2, fx1:fx2]
if lp_crop.size != 0:
ok2, buf2 = cv2.imencode(".jpg", lp_crop, [cv2.IMWRITE_JPEG_QUALITY, 80])
if ok2:
car_b64 = base64.b64encode(buf2).decode("utf-8")
entry = {
"tid": tid,
"lp": b64,
"ocr": "12머 3532"
}
if car_b64 is not None:
entry["car"] = car_b64
if feat_idx == FEAT_ABNORM:
entry = {
"tid": tid,
"fallen": b64
}
send_list.append(entry)
if send_list:
meta = {
"type": "card",
"timestamp": int(now * 1000),
"items": send_list,
}
if loop_main:
asyncio.run_coroutine_threadsafe(
ws_broadcast_text(json.dumps(meta)),
loop_main
)
time.sleep(1.0)
async def main_async():
global loop_main
@ -387,6 +816,9 @@ async def main_async():
t_uds = threading.Thread(target=uds_loop, daemon=True)
t_uds.start()
t_card = threading.Thread(target=card_sender_loop, daemon=True)
t_card.start()
t2 = threading.Thread(target=uds_stats_printer_loop, daemon=True)
t2.start()

Loading…
Cancel
Save