Tracker more sensitive and cv_renderer animates lines

This commit is contained in:
Ruben van de Ven 2024-12-23 15:35:51 +01:00
parent 888ef7ff93
commit cebe102e74
10 changed files with 211 additions and 106 deletions

View file

@ -2,10 +2,10 @@
# Default YOLO tracker settings for ByteTrack tracker https://github.com/ifzhang/ByteTrack # Default YOLO tracker settings for ByteTrack tracker https://github.com/ifzhang/ByteTrack
tracker_type: bytetrack # tracker type, ['botsort', 'bytetrack'] tracker_type: bytetrack # tracker type, ['botsort', 'bytetrack']
track_high_thresh: 0.05 # threshold for the first association track_high_thresh: 0.0001 # threshold for the first association
track_low_thresh: 0.01 # threshold for the second association track_low_thresh: 0.0001 # threshold for the second association
new_track_thresh: 0.1 # threshold for init new track if the detection does not match any tracks new_track_thresh: 0.0001 # threshold for init new track if the detection does not match any tracks
track_buffer: 35 # buffer to calculate the time when to remove tracks track_buffer: 50 # buffer to calculate the time when to remove tracks
match_thresh: 0.9 # threshold for matching tracks match_thresh: 0.95 # threshold for matching tracks
fuse_score: True # Whether to fuse confidence scores with the iou distances before matching fuse_score: True # Whether to fuse confidence scores with the iou distances before matching
# min_box_area: 10 # threshold for min box areas(for tracker evaluation, not used for now) # min_box_area: 10 # threshold for min box areas(for tracker evaluation, not used for now)

View file

@ -54,10 +54,10 @@ class AnimationRenderer:
self.tracker_sock.setsockopt(zmq.SUBSCRIBE, b'') self.tracker_sock.setsockopt(zmq.SUBSCRIBE, b'')
self.tracker_sock.connect(config.zmq_trajectory_addr) self.tracker_sock.connect(config.zmq_trajectory_addr)
self.frame_sock = context.socket(zmq.SUB) self.frame_noimg_sock = context.socket(zmq.SUB)
self.frame_sock.setsockopt(zmq.CONFLATE, 1) # only keep latest frame. NB. make sure this comes BEFORE connect, otherwise it's ignored!! self.frame_noimg_sock.setsockopt(zmq.CONFLATE, 1) # only keep latest frame. NB. make sure this comes BEFORE connect, otherwise it's ignored!!
self.frame_sock.setsockopt(zmq.SUBSCRIBE, b'') self.frame_noimg_sock.setsockopt(zmq.SUBSCRIBE, b'')
self.frame_sock.connect(config.zmq_frame_addr) self.frame_noimg_sock.connect(config.zmq_frame_noimg_addr)
self.H = self.config.H self.H = self.config.H
@ -324,26 +324,27 @@ class AnimationRenderer:
new_tracks = False new_tracks = False
try: try:
self.frame: Frame = self.frame_sock.recv_pyobj(zmq.NOBLOCK) self.frame: Frame = self.frame_noimg_sock.recv_pyobj(zmq.NOBLOCK)
if not self.first_time: if not self.first_time:
self.first_time = self.frame.time self.first_time = self.frame.time
img = self.frame.img if self.frame.img:
# newcameramtx, roi = cv2.getOptimalNewCameraMatrix(self.config.camera.mtx, self.config.camera.dist, (self.frame.img.shape[1], self.frame.img.shape[0]), 1, (self.frame.img.shape[1], self.frame.img.shape[0])) img = self.frame.img
img = cv2.undistort(img, self.config.camera.mtx, self.config.camera.dist, None, self.config.camera.newcameramtx) # newcameramtx, roi = cv2.getOptimalNewCameraMatrix(self.config.camera.mtx, self.config.camera.dist, (self.frame.img.shape[1], self.frame.img.shape[0]), 1, (self.frame.img.shape[1], self.frame.img.shape[0]))
img = cv2.warpPerspective(img, convert_world_space_to_img_space(self.config.camera.H), (self.config.camera.w, self.config.camera.h)) img = cv2.undistort(img, self.config.camera.mtx, self.config.camera.dist, None, self.config.camera.newcameramtx)
# img = cv2.GaussianBlur(img, (15, 15), 0) img = cv2.warpPerspective(img, convert_world_space_to_img_space(self.config.camera.H), (self.config.camera.w, self.config.camera.h))
img = cv2.flip(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 0) # img = cv2.GaussianBlur(img, (15, 15), 0)
img = pyglet.image.ImageData(self.frame_size[0], self.frame_size[1], 'RGB', img.tobytes()) img = cv2.flip(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 0)
# don't draw in batch, so that it is the background img = pyglet.image.ImageData(self.frame_size[0], self.frame_size[1], 'RGB', img.tobytes())
if hasattr(self, 'video_sprite') and self.video_sprite: # don't draw in batch, so that it is the background
self.video_sprite.delete() if hasattr(self, 'video_sprite') and self.video_sprite:
self.frame.img = None self.video_sprite.delete()
self.frame.img = None
self.video_sprite = pyglet.sprite.Sprite(img=img, batch=self.batch_bg) self.video_sprite = pyglet.sprite.Sprite(img=img, batch=self.batch_bg)
# transform to flipped coordinate system for pyglet # transform to flipped coordinate system for pyglet
self.video_sprite.y = self.window.height - self.video_sprite.height self.video_sprite.y = self.window.height - self.video_sprite.height
# self.frame.img = np.array([]) # clearing memory? # self.frame.img = np.array([]) # clearing memory?
# self.video_sprite.opacity = 70 # self.video_sprite.opacity = 70
except zmq.ZMQError as e: except zmq.ZMQError as e:
# idx = frame.index if frame else "NONE" # idx = frame.index if frame else "NONE"
# logger.debug(f"reuse video frame {idx}") # logger.debug(f"reuse video frame {idx}")

View file

@ -253,6 +253,11 @@ connection_parser.add_argument('--zmq-frame-addr',
type=str, type=str,
default="ipc:///tmp/feeds_frame") default="ipc:///tmp/feeds_frame")
connection_parser.add_argument('--zmq-frame-noimg-addr',
help='Manually specity communication addr for the frame messages',
type=str,
default="ipc:///tmp/feeds_frame2")
connection_parser.add_argument('--ws-port', connection_parser.add_argument('--ws-port',
help='Port to listen for incomming websocket connections. Also serves the testing html-page.', help='Port to listen for incomming websocket connections. Also serves the testing html-page.',

View file

@ -334,7 +334,7 @@ class CvRenderer:
def run(self): def run(self, timer_counter):
frame = None frame = None
prediction_frame = None prediction_frame = None
tracker_frame = None tracker_frame = None
@ -349,6 +349,8 @@ class CvRenderer:
while self.is_running.is_set(): while self.is_running.is_set():
i+=1 i+=1
with timer_counter.get_lock():
timer_counter.value+=1
# zmq_ev = self.frame_sock.poll(timeout=2000) # zmq_ev = self.frame_sock.poll(timeout=2000)
@ -406,10 +408,10 @@ class CvRenderer:
# clear out old tracks & predictions: # clear out old tracks & predictions:
for track_id, track in list(self.tracks.items()): for track_id, track in list(self.tracks.items()):
if get_opacity(track, frame) == 0: if get_animation_position(track, frame) == 1:
self.tracks.pop(track_id) self.tracks.pop(track_id)
for prediction_id, track in list(self.predictions.items()): for prediction_id, track in list(self.predictions.items()):
if get_opacity(track, frame) == 0: if get_animation_position(track, frame) == 1:
self.predictions.pop(prediction_id) self.predictions.pop(prediction_id)
logger.info('Stopping') logger.info('Stopping')
@ -443,10 +445,10 @@ colorset = [
# (0,0,0), # (0,0,0),
# ] # ]
def get_opacity(track: Track, current_frame: Frame): def get_animation_position(track: Track, current_frame: Frame):
fade_duration = current_frame.camera.fps * 1.5 fade_duration = current_frame.camera.fps * 3
diff = current_frame.index - track.history[-1].frame_nr diff = current_frame.index - track.history[-1].frame_nr
return max(0, 1 - diff / fade_duration) return max(0, min(1, diff / fade_duration))
# track.history[-1].frame_nr < (current_frame.index - current_frame.camera.fps * 3) # track.history[-1].frame_nr < (current_frame.index - current_frame.camera.fps * 3)
# track.history[-1].frame_nr < (current_frame.index - current_frame.camera.fps * 3) # track.history[-1].frame_nr < (current_frame.index - current_frame.camera.fps * 3)
@ -497,8 +499,8 @@ def decorate_frame(frame: Frame, tracker_frame: Frame, prediction_frame: Frame,
inv_H = np.linalg.pinv(prediction_frame.H) inv_H = np.linalg.pinv(prediction_frame.H)
# draw_track(img, track, int(track_id)) # draw_track(img, track, int(track_id))
draw_trackjectron_history(img, track, int(track.track_id), convert_world_points_to_img_points) draw_trackjectron_history(img, track, int(track.track_id), convert_world_points_to_img_points)
opacity = get_opacity(track, frame) anim_position = get_animation_position(track, frame)
draw_track_predictions(img, track, int(track.track_id)+1, config.camera, convert_world_points_to_img_points, opacity=opacity) draw_track_predictions(img, track, int(track.track_id)+1, config.camera, convert_world_points_to_img_points, anim_position=anim_position)
cv2.putText(img, f"{len(track.predictor_history) if track.predictor_history else 'none'}", to_point(track.history[0].get_foot_coords()), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 1) cv2.putText(img, f"{len(track.predictor_history) if track.predictor_history else 'none'}", to_point(track.history[0].get_foot_coords()), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 1)
base_color = (255,)*3 base_color = (255,)*3
@ -532,6 +534,6 @@ def decorate_frame(frame: Frame, tracker_frame: Frame, prediction_frame: Frame,
return img return img
def run_cv_renderer(config: Namespace, is_running: BaseEvent): def run_cv_renderer(config: Namespace, is_running: BaseEvent, timer_counter):
renderer = CvRenderer(config, is_running) renderer = CvRenderer(config, is_running)
renderer.run() renderer.run(timer_counter)

View file

@ -420,6 +420,9 @@ class Frame:
} for t in self.tracks.values() } for t in self.tracks.values()
} }
def without_img(self):
return Frame(self.index, None, self.time, self.tracks, self.H, self.camera)
def video_src_from_config(config) -> UrlOrPath: def video_src_from_config(config) -> UrlOrPath:
if config.video_loop: if config.video_loop:
video_srcs: Iterable[UrlOrPath] = cycle(config.video_src) video_srcs: Iterable[UrlOrPath] = cycle(config.video_src)
@ -442,12 +445,17 @@ class FrameEmitter:
self.frame_sock.setsockopt(zmq.CONFLATE, 1) # only keep latest frame. make sure to set BEFORE connect/bind self.frame_sock.setsockopt(zmq.CONFLATE, 1) # only keep latest frame. make sure to set BEFORE connect/bind
self.frame_sock.bind(config.zmq_frame_addr) self.frame_sock.bind(config.zmq_frame_addr)
self.frame_noimg_sock = context.socket(zmq.PUB)
self.frame_noimg_sock.setsockopt(zmq.CONFLATE, 1) # only keep latest frame. make sure to set BEFORE connect/bind
self.frame_noimg_sock.bind(config.zmq_frame_noimg_addr)
logger.info(f"Connection socket {config.zmq_frame_addr}") logger.info(f"Connection socket {config.zmq_frame_addr}")
logger.info(f"Connection socket {config.zmq_frame_noimg_addr}")
self.video_srcs = video_src_from_config(self.config) self.video_srcs = video_src_from_config(self.config)
def emit_video(self): def emit_video(self, timer_counter):
i = 0 i = 0
delay_generation = False delay_generation = False
for video_path in self.video_srcs: for video_path in self.video_srcs:
@ -462,7 +470,7 @@ class FrameEmitter:
video.set(cv2.CAP_PROP_FPS, 5) video.set(cv2.CAP_PROP_FPS, 5)
fps=5 fps=5
elif video_path.url.scheme == 'rtsp': elif video_path.url.scheme == 'rtsp':
gst = f"rtspsrc location={video_path} latency=0 buffer-mode=auto ! decodebin ! videoconvert ! appsink max-buffers=1 drop=true" gst = f"rtspsrc location={video_path} latency=0 buffer-mode=auto ! decodebin ! videoconvert ! appsink max-buffers=0 drop=true"
logger.info(f"Capture gstreamer (gst-launch-1.0): {gst}") logger.info(f"Capture gstreamer (gst-launch-1.0): {gst}")
video = cv2.VideoCapture(gst, cv2.CAP_GSTREAMER) video = cv2.VideoCapture(gst, cv2.CAP_GSTREAMER)
fps=12 fps=12
@ -496,7 +504,11 @@ class FrameEmitter:
prev_time = time.time() prev_time = time.time()
while self.is_running.is_set(): while self.is_running.is_set():
with timer_counter.get_lock():
timer_counter.value += 1
ret, img = video.read() ret, img = video.read()
# seek to 0 if video has finished. Infinite loop # seek to 0 if video has finished. Infinite loop
@ -515,6 +527,7 @@ class FrameEmitter:
frame = Frame(index=i, img=img, H=self.config.H, camera=self.config.camera) frame = Frame(index=i, img=img, H=self.config.H, camera=self.config.camera)
# TODO: this is very dirty, need to find another way. # TODO: this is very dirty, need to find another way.
# perhaps multiprocessing Array? # perhaps multiprocessing Array?
self.frame_noimg_sock.send(pickle.dumps(frame.without_img()))
self.frame_sock.send(pickle.dumps(frame)) self.frame_sock.send(pickle.dumps(frame))
# only delay consuming the next frame when using a file. # only delay consuming the next frame when using a file.
@ -540,7 +553,7 @@ class FrameEmitter:
def run_frame_emitter(config: Namespace, is_running: Event): def run_frame_emitter(config: Namespace, is_running: Event, timer_counter: int):
router = FrameEmitter(config, is_running) router = FrameEmitter(config, is_running)
router.emit_video() router.emit_video(timer_counter)
is_running.clear() is_running.clear()

View file

@ -13,6 +13,7 @@ from trap.prediction_server import run_prediction_server
from trap.preview_renderer import run_preview_renderer from trap.preview_renderer import run_preview_renderer
from trap.animation_renderer import run_animation_renderer from trap.animation_renderer import run_animation_renderer
from trap.socket_forwarder import run_ws_forwarder from trap.socket_forwarder import run_ws_forwarder
from trap.timer import TimerCollection
from trap.tracker import run_tracker from trap.tracker import run_tracker
from setproctitle import setproctitle, setthreadtitle from setproctitle import setproctitle, setthreadtitle
@ -83,20 +84,23 @@ def start():
# queue_listener.handlers.append(socket_handler) # queue_listener.handlers.append(socket_handler)
timers = TimerCollection()
timer_fe = timers.new('frame_emitter')
timer_tracker = timers.new('tracker')
# instantiating process with arguments # instantiating process with arguments
procs = [ procs = [
# ExceptionHandlingProcess(target=run_ws_forwarder, kwargs={'config': args, 'is_running': isRunning}, name='forwarder'), # ExceptionHandlingProcess(target=run_ws_forwarder, kwargs={'config': args, 'is_running': isRunning}, name='forwarder'),
ExceptionHandlingProcess(target=run_frame_emitter, kwargs={'config': args, 'is_running': isRunning}, name='frame_emitter'), ExceptionHandlingProcess(target=run_frame_emitter, kwargs={'config': args, 'is_running': isRunning, 'timer_counter': timer_fe.iterations}, name='frame_emitter'),
ExceptionHandlingProcess(target=run_tracker, kwargs={'config': args, 'is_running': isRunning}, name='tracker'), ExceptionHandlingProcess(target=run_tracker, kwargs={'config': args, 'is_running': isRunning, 'timer_counter': timer_tracker.iterations}, name='tracker'),
] ]
# if args.render_file or args.render_url or args.render_window: # if args.render_file or args.render_url or args.render_window:
if args.render_window or args.render_file or args.render_url: if args.render_window or args.render_file or args.render_url:
timer_preview = timers.new('preview')
procs.append( procs.append(
# ExceptionHandlingProcess(target=run_cv_renderer, kwargs={'config': args, 'is_running': isRunning}, name='preview') # ExceptionHandlingProcess(target=run_cv_renderer, kwargs={'config': args, 'is_running': isRunning}, name='preview')
ExceptionHandlingProcess(target=run_cv_renderer, kwargs={'config': args, 'is_running': isRunning}, name='preview') ExceptionHandlingProcess(target=run_cv_renderer, kwargs={'config': args, 'is_running': isRunning, 'timer_counter': timer_preview.iterations}, name='preview')
) )
if args.render_animation: if args.render_animation:
procs.append( procs.append(
@ -104,8 +108,19 @@ def start():
) )
if not args.bypass_prediction: if not args.bypass_prediction:
timer_predict = timers.new('predict')
procs.append( procs.append(
ExceptionHandlingProcess(target=run_prediction_server, kwargs={'config': args, 'is_running':isRunning}, name='inference'), ExceptionHandlingProcess(target=run_prediction_server, kwargs={'config': args, 'is_running':isRunning, 'timer_counter': timer_predict.iterations}, name='inference'),
)
def timer_process(timers: TimerCollection, is_running: Event):
while is_running.is_set():
time.sleep(1)
timers.snapshot()
print(timers.to_string())
procs.append(
ExceptionHandlingProcess(target=timer_process, kwargs={'is_running':isRunning, 'timers': timers}, name='timer'),
) )
try: try:

View file

@ -166,11 +166,11 @@ class PredictionServer:
def send_frame(self, frame: Frame): def send_frame(self, frame: Frame):
if self.external_predictions: if self.external_predictions:
# data = json.dumps(frame, cls=DataclassJSONEncoder) # data = json.dumps(frame, cls=DataclassJSONEncoder)
self.prediction_socket.send_json(frame, cls=DataclassJSONEncoder) self.prediction_socket.send_json(obj=frame, cls=DataclassJSONEncoder)
else: else:
self.prediction_socket.send_pyobj(frame) self.prediction_socket.send_pyobj(frame)
def run(self): def run(self, timer_counter):
print(self.config) print(self.config)
if self.config.seed is not None: if self.config.seed is not None:
random.seed(self.config.seed) random.seed(self.config.seed)
@ -247,6 +247,8 @@ class PredictionServer:
prev_run_time = 0 prev_run_time = 0
while self.is_running.is_set(): while self.is_running.is_set():
timestep += 1 timestep += 1
with timer_counter.get_lock():
timer_counter.value+=1
# this_run_time = time.time() # this_run_time = time.time()
# logger.debug(f'test {prev_run_time - this_run_time}') # logger.debug(f'test {prev_run_time - this_run_time}')
@ -477,7 +479,7 @@ class PredictionServer:
def run_prediction_server(config: Namespace, is_running: Event): def run_prediction_server(config: Namespace, is_running: Event, timer_counter):
# attempt to trace the warnings coming from pytorch # attempt to trace the warnings coming from pytorch
# def warn_with_traceback(message, category, filename, lineno, file=None, line=None): # def warn_with_traceback(message, category, filename, lineno, file=None, line=None):
@ -488,4 +490,4 @@ def run_prediction_server(config: Namespace, is_running: Event):
# warnings.showwarning = warn_with_traceback # warnings.showwarning = warn_with_traceback
s = PredictionServer(config, is_running) s = PredictionServer(config, is_running)
s.run() s.run(timer_counter)

View file

@ -1,6 +1,9 @@
import collections
from re import A
import time import time
from multiprocessing.sharedctypes import RawValue, Value, Array from multiprocessing.sharedctypes import RawValue, Value, Array
from ctypes import c_double from ctypes import c_double
from typing import MutableSequence
class Timer(): class Timer():
@ -8,47 +11,58 @@ class Timer():
Measure 2 independent things: the freuency of tic, and the duration of tic->toc Measure 2 independent things: the freuency of tic, and the duration of tic->toc
Note that indeed these don't need to be equal Note that indeed these don't need to be equal
""" """
def __init__(self) -> None: def __init__(self, name = 'timer') -> None:
self.last_tic = RawValue(c_double, -1) self.name = name
self.last_toc = RawValue(c_double, -1) self.tocs: MutableSequence[(float, int)] = collections.deque(maxlen=5)
self.fps = RawValue(c_double, -1) self.iterations = Value('i', 0)
self.processing_duration = RawValue(c_double, -1)
self.smoothing = .1 # def tic(self):
# now = time.time()
# if self.last_tic is None:
def tic(self): # self.last_tic = now
now = time.time() # return
if self.last_tic is None:
self.last_tic = now # duration = now - self.last_tic
return # self.last_tic = now
duration = now - self.last_tic # current_fps = 1 / duration
self.last_tic = now # if not self.fps:
# self.fps = current_fps
current_fps = 1 / duration # else:
if not self.fps: # self.fps = self.fps * (1-self.smoothing) + current_fps * self.smoothing
self.fps = current_fps
else:
self.fps = self.fps * (1-self.smoothing) + current_fps * self.smoothing
def toc(self): def toc(self):
self.last_toc = time.time() self.iterations += 1
duration = self.last_toc - self.last_tic
self.processing_duration = self.processing_duration * (1-self.smoothing) + duration * self.smoothing
def snapshot(self):
self.tocs.append((time.perf_counter(), self.iterations.value))
@property @property
def fps(self): def fps(self):
fpses = []
if len(self.tocs) < 2:
return 0
dt = self.tocs[-1][0] - self.tocs[0][0]
di = self.tocs[-1][1] - self.tocs[0][1]
return di/dt
pass
class TimerCollection(): class TimerCollection():
def __init__(self) -> None: def __init__(self) -> None:
self._timers = set() self._timers = set()
def print(self)->str: def snapshot(self):
print('Update', end='\r') for timer in self._timers:
timer.snapshot()
def to_string(self)->str:
strs = [f"{t.name} {t.fps:.2f}" for t in self._timers]
return " ".join(strs)
def new(self, name='timer'):
t = Timer(name)
self._timers.add(t)
return t

View file

@ -22,7 +22,7 @@ from ultralytics import YOLO
from ultralytics.engine.results import Results as YOLOResult from ultralytics.engine.results import Results as YOLOResult
import tqdm import tqdm
from trap.utils import lerp from trap.utils import inv_lerp, lerp
@ -185,40 +185,82 @@ def tracker_compare():
bar.set_description(f"[{frames.video_nr}/{len(frames.video_srcs)}] [{frames.frame_idx}/{frames.frame_count}] {str(frames.video_path)}") bar.set_description(f"[{frames.video_nr}/{len(frames.video_srcs)}] [{frames.frame_idx}/{frames.frame_count}] {str(frames.video_path)}")
def draw_track_predictions(img: cv2.Mat, track: Track, color_index: int, camera:Camera, convert_points: Optional[Callable], opacity=1): def transition_path_points(path: np.array, t: float):
""" """
Opacity: 0-1 """
if t >= 1:
return path
if t <= 0:
return np.array([path[0]])
# new_path = np.array([])
lengths = np.sqrt(np.sum(np.diff(path, axis=0)**2, axis=1))
cum_lenghts = np.cumsum(lengths)
# distance = cum_lenghts[-1] * t
ts = np.concatenate((np.array([0.]), cum_lenghts / cum_lenghts[-1]))
new_path = [path[0]]
for a, b, t_a, t_b in zip(path[:-1], path[1:], ts[:-1], ts[1:]):
if t_b < t:
new_path.append(b)
continue
# interpolate
relative_t = inv_lerp(t_a, t_b, t)
x = lerp(a[0], b[0], relative_t)
y = lerp(a[1], b[1], relative_t)
print(relative_t, a , b, x, y)
new_path.append([x,y])
break
return np.array(new_path)
def draw_track_predictions(img: cv2.Mat, track: Track, color_index: int, camera:Camera, convert_points: Optional[Callable], anim_position=.8):
"""
anim_position: 0-1
""" """
if not track.predictions: if not track.predictions:
return return
current_point = track.get_projected_history(camera=camera)[-1] current_point = track.get_projected_history(camera=camera)[-1]
if convert_points: opacity = 1-min(1, max(0, inv_lerp(0.8, 1, anim_position))) # fade out
current_point = convert_points([current_point])[0] slide_t = min(1, max(0, inv_lerp(0, 0.8, anim_position))) # slide_position
# if convert_points:
# current_point = convert_points([current_point])[0]
for pred_i, pred in enumerate(track.predictions): for pred_i, pred in enumerate(track.predictions):
pred_coords = pred #cv2.perspectiveTransform(np.array([pred]), inv_H)[0].tolist() pred_coords = pred #cv2.perspectiveTransform(np.array([pred]), inv_H)[0].tolist()
# line_points = np.concatenate(([current_point], pred_coords)) # 'current point' is amoving target
line_points = pred_coords
# print(pred_coords, current_point, line_points)
line_points = transition_path_points(line_points, slide_t)
if convert_points: if convert_points:
pred_coords = convert_points(pred_coords) line_points = convert_points(line_points)
# color = (128,0,128) if pred_i else (128,128,0) # color = (128,0,128) if pred_i else (128,128,0)
color = bgr_colors[color_index % len(bgr_colors)] color = bgr_colors[color_index % len(bgr_colors)]
color = tuple([int(c*opacity) for c in color]) color = tuple([int(c*opacity) for c in color])
for ci in range(0, len(pred_coords)): for start, end in zip(line_points[:-1], line_points[1:]):
if ci == 0: # for ci in range(0, len(pred_coords)):
# TODO)) prev point # if ci == 0:
# continue # # TODO)) prev point
start = [int(p) for p in current_point] # # continue
# start = [int(p) for p in coords[-1]] # start = [int(p) for p in current_point]
# start = [0,0]? # # start = [int(p) for p in coords[-1]]
# print(start) # # start = [0,0]?
else: # # print(start)
start = [int(p) for p in pred_coords[ci-1]] # else:
end = [int(p) for p in pred_coords[ci]] # start = [int(p) for p in pred_coords[ci-1]]
cv2.line(img, start, end, color, 1, lineType=cv2.LINE_AA) # end = [int(p) for p in pred_coords[ci]]
# print(np.rint(start),np.rint(end).tolist())
cv2.line(img, np.rint(start).astype(int), np.rint(end).astype(int), color, 1, lineType=cv2.LINE_AA)
# cv2.circle(img, end, 2, color, 1, lineType=cv2.LINE_AA) # cv2.circle(img, end, 2, color, 1, lineType=cv2.LINE_AA)
def draw_trackjectron_history(img: cv2.Mat, track: Track, color_index: int, convert_points: Optional[Callable]): def draw_trackjectron_history(img: cv2.Mat, track: Track, color_index: int, convert_points: Optional[Callable]):

View file

@ -9,7 +9,7 @@ from multiprocessing import Event
from pathlib import Path from pathlib import Path
import pickle import pickle
import time import time
from typing import Optional, List from typing import Dict, Optional, List
import jsonlines import jsonlines
import numpy as np import numpy as np
import torch import torch
@ -61,7 +61,7 @@ RCNN_SCALE = .4 # seems to have no impact on detections in the corners
def _yolov8_track(frame: Frame, model: YOLO, **kwargs) -> List[Detection]: def _yolov8_track(frame: Frame, model: YOLO, **kwargs) -> List[Detection]:
results: List[YOLOResult] = list(model.track(frame.img, persist=True, tracker="custom_bytetrack.yaml", verbose=False, **kwargs)) results: List[YOLOResult] = list(model.track(frame.img, persist=True, tracker="custom_bytetrack.yaml", verbose=False, conf=0.00001, **kwargs))
if results[0].boxes is None or results[0].boxes.id is None: if results[0].boxes is None or results[0].boxes.id is None:
# work around https://github.com/ultralytics/ultralytics/issues/5968 # work around https://github.com/ultralytics/ultralytics/issues/5968
return [] return []
@ -99,6 +99,10 @@ class TrackFilter:
def apply(self, tracks: List[Track], camera: Camera): def apply(self, tracks: List[Track], camera: Camera):
return [t for t in tracks if self.filter(t, camera)] return [t for t in tracks if self.filter(t, camera)]
def apply_to_dict(self, tracks: Dict[str, Track], camera: Camera):
tracks = self.apply(tracks.values(), camera)
return {t.track_id: t for t in tracks}
class FinalDisplacementFilter(TrackFilter): class FinalDisplacementFilter(TrackFilter):
def __init__(self, min_displacement): def __init__(self, min_displacement):
self.min_displacement = min_displacement self.min_displacement = min_displacement
@ -444,7 +448,7 @@ class Tracker:
def track_frame(self, frame: Frame): def track_frame(self, frame: Frame):
if self.config.detector == DETECTOR_YOLOv8: if self.config.detector == DETECTOR_YOLOv8:
detections: List[Detection] = _yolov8_track(frame, self.model, classes=[0], imgsz=[1152, 640]) detections: List[Detection] = _yolov8_track(frame, self.model, classes=[0, 15, 16], imgsz=[1152, 640])
else : else :
detections: List[Detection] = self._resnet_track(frame, scale = RCNN_SCALE) detections: List[Detection] = self._resnet_track(frame, scale = RCNN_SCALE)
@ -458,7 +462,7 @@ class Tracker:
return detections return detections
def track(self, is_running: Event): def track(self, is_running: Event, timer_counter: int = 0):
""" """
Live tracking of frames coming in over zmq Live tracking of frames coming in over zmq
""" """
@ -500,7 +504,11 @@ class Tracker:
end_time = None end_time = None
tracker_dt = None tracker_dt = None
w_time = None w_time = None
displacement_filter = FinalDisplacementFilter(.2)
while self.is_running.is_set(): while self.is_running.is_set():
with timer_counter.get_lock():
timer_counter.value += 1
# this waiting for target_dt causes frame loss. E.g. with target_dt at .1, it # this waiting for target_dt causes frame loss. E.g. with target_dt at .1, it
# skips exactly 1 frame on a 10 fps video (which, it obviously should not do) # skips exactly 1 frame on a 10 fps video (which, it obviously should not do)
# so for now, timing should move to emitter # so for now, timing should move to emitter
@ -566,6 +574,9 @@ class Tracker:
# } # }
active_track_ids = [d.track_id for d in detections] active_track_ids = [d.track_id for d in detections]
active_tracks = {t.track_id: t.get_with_interpolated_history() for t in self.tracks.values() if t.track_id in active_track_ids} active_tracks = {t.track_id: t.get_with_interpolated_history() for t in self.tracks.values() if t.track_id in active_track_ids}
active_tracks = displacement_filter.apply_to_dict(active_tracks, frame.camera)# a filter to remove just detecting static objects
# active_tracks = {t.track_id: t for t in self.tracks.values() if t.track_id in active_track_ids} # active_tracks = {t.track_id: t for t in self.tracks.values() if t.track_id in active_track_ids}
# active_tracks = {t.track_id: t for t in self.tracks.values() if t.track_id in active_track_ids} # active_tracks = {t.track_id: t for t in self.tracks.values() if t.track_id in active_track_ids}
# logger.info(f"{trajectories}") # logger.info(f"{trajectories}")
@ -579,7 +590,7 @@ class Tracker:
frame = self.smoother.smooth_frame_tracks(frame) frame = self.smoother.smooth_frame_tracks(frame)
# print(f"send to {self.trajectory_socket}, {self.config.zmq_trajectory_addr}") # print(f"send to {self.trajectory_socket}, {self.config.zmq_trajectory_addr}")
self.trajectory_socket.send_pyobj(frame) self.trajectory_socket.send_pyobj(frame.without_img()) # ditch image for faster passthrough
end_time = time.time() end_time = time.time()
tracker_dt = end_time - start_time tracker_dt = end_time - start_time
@ -665,9 +676,9 @@ class Tracker:
return [([d[0], d[1], d[2]-d[0], d[3]-d[1]], d[4], d[5]) for d in detections] return [([d[0], d[1], d[2]-d[0], d[3]-d[1]], d[4], d[5]) for d in detections]
def run_tracker(config: Namespace, is_running: Event): def run_tracker(config: Namespace, is_running: Event, timer_counter):
router = Tracker(config) router = Tracker(config)
router.track(is_running) router.track(is_running, timer_counter)