Reuse shared memory segments

This commit is contained in:
Michael Wei 2020-11-28 01:51:14 +00:00
parent 893e6b40a7
commit e78cec0f10
4 changed files with 143 additions and 47 deletions

View File

@ -116,8 +116,7 @@ def run_detector(detection_queue, out_events: Dict[str, mp.Event], avg_speed, st
while True: while True:
connection_id = detection_queue.get() connection_id = detection_queue.get()
input_frame = frame_manager.get(connection_id, (1,300,300,3)) input_frame = frame_manager.get((1,300,300,3), name=connection_id)
if input_frame is None: if input_frame is None:
continue continue

View File

@ -61,7 +61,7 @@ class CameraState():
self._current_frame = np.zeros((self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1]), np.uint8) self._current_frame = np.zeros((self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1]), np.uint8)
self.current_frame_lock = threading.Lock() self.current_frame_lock = threading.Lock()
self.current_frame_time = 0.0 self.current_frame_time = 0.0
self.previous_frame_id = None self.previous_frame_idx = None
self.callbacks = defaultdict(lambda: []) self.callbacks = defaultdict(lambda: [])
def get_current_frame(self, draw=False): def get_current_frame(self, draw=False):
@ -120,11 +120,11 @@ class CameraState():
def on(self, event_type: str, callback: Callable[[Dict], None]): def on(self, event_type: str, callback: Callable[[Dict], None]):
self.callbacks[event_type].append(callback) self.callbacks[event_type].append(callback)
def update(self, frame_time, tracked_objects): def update(self, idx, frame_time, tracked_objects):
self.current_frame_time = frame_time self.current_frame_time = frame_time
# get the new frame and delete the old frame # get the new frame and delete the old frame
frame_id = f"{self.name}{frame_time}" frame_id = f"{self.name}{frame_time}"
current_frame = self.frame_manager.get(frame_id, (self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1])) current_frame = self.frame_manager.get((self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1]), idx)
current_ids = tracked_objects.keys() current_ids = tracked_objects.keys()
previous_ids = self.tracked_objects.keys() previous_ids = self.tracked_objects.keys()
@ -237,9 +237,9 @@ class CameraState():
with self.current_frame_lock: with self.current_frame_lock:
self._current_frame = current_frame self._current_frame = current_frame
if not self.previous_frame_id is None: if not self.previous_frame_idx is None:
self.frame_manager.delete(self.previous_frame_id) self.frame_manager.close(self.previous_frame_idx)
self.previous_frame_id = frame_id self.previous_frame_idx = idx
class TrackedObjectProcessor(threading.Thread): class TrackedObjectProcessor(threading.Thread):
def __init__(self, camera_config, client, topic_prefix, tracked_objects_queue, event_queue, stop_event): def __init__(self, camera_config, client, topic_prefix, tracked_objects_queue, event_queue, stop_event):
@ -251,7 +251,6 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue = event_queue self.event_queue = event_queue
self.stop_event = stop_event self.stop_event = stop_event
self.camera_states: Dict[str, CameraState] = {} self.camera_states: Dict[str, CameraState] = {}
self.frame_manager = SharedMemoryFrameManager()
def start(camera, obj): def start(camera, obj):
# publish events to mqtt # publish events to mqtt
@ -303,7 +302,7 @@ class TrackedObjectProcessor(threading.Thread):
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False) self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
for camera in self.camera_config.keys(): for camera in self.camera_config.keys():
camera_state = CameraState(camera, self.camera_config[camera], self.frame_manager) camera_state = CameraState(camera, self.camera_config[camera], SharedMemoryFrameManager(camera))
camera_state.on('start', start) camera_state.on('start', start)
camera_state.on('update', update) camera_state.on('update', update)
camera_state.on('end', end) camera_state.on('end', end)
@ -363,13 +362,13 @@ class TrackedObjectProcessor(threading.Thread):
break break
try: try:
camera, frame_time, current_tracked_objects = self.tracked_objects_queue.get(True, 10) camera, idx, frame_time, current_tracked_objects = self.tracked_objects_queue.get(True, 10)
except queue.Empty: except queue.Empty:
continue continue
camera_state = self.camera_states[camera] camera_state = self.camera_states[camera]
camera_state.update(frame_time, current_tracked_objects) camera_state.update(idx, frame_time, current_tracked_objects)
# update zone status for each label # update zone status for each label
for zone in camera_state.config['zones'].keys(): for zone in camera_state.config['zones'].keys():

View File

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import datetime import datetime
from io import UnsupportedOperation
import time import time
import signal import signal
import traceback import traceback
@ -10,7 +11,7 @@ import threading
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import hashlib import hashlib
from multiprocessing import shared_memory from multiprocessing import shared_memory
from typing import AnyStr from typing import AnyStr, Optional, Tuple
def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'): def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'):
if color is None: if color is None:
@ -216,28 +217,128 @@ class DictFrameManager(FrameManager):
del self.frames[name] del self.frames[name]
class SharedMemoryFrameManager(FrameManager): class SharedMemoryFrameManager(FrameManager):
def __init__(self):
self.shm_store = {}
def create(self, name, size) -> AnyStr: """Initialize a SharedMemoryFrameManager.
shm = shared_memory.SharedMemory(name=name, create=True, size=size) Frames are stored as SharedMemory segments, named <name>_<slot>
self.shm_store[name] = shm The first byte of each slot represents whether the segment is "freed".
return shm.buf 0 means the segment is free and available for use
Any other value means the segment is in use.
def get(self, name, shape): Args:
if name in self.shm_store: name: The name of the instance to open.
shm = self.shm_store[name] If not set, a name argument *must* be passed in to each operation.
size: The size of frames created by this frame manager.
If not set, the frame manager will be read only.
"""
def __init__(self, name: Optional[str] = None, size : Optional[int]= None):
# list of opened shms
self.shm_lists = {}
# If name is set, create the dict for the default namespace
self.shm_lists[name] = {}
self.name = name
self.size = size
"""Create a new frame.
Returns:
A tuple containing the allocated frame id and the frame itself.
Raises:
UnsupportedOperation: If the size of the frame manager is not set.
"""
def create(self, name = None) -> Tuple[int, AnyStr]:
if name is None:
name = self.name
if name is None:
raise UnsupportedOperation("Cannot create without name")
if self.size is None:
raise UnsupportedOperation("Cannot create new frame without size set")
if name not in self.shm_lists:
self.shm_lists[name] = {}
shm_list = self.shm_lists[name]
# Check if we have any free SHMs in our list
for idx in shm_list:
shm = shm_list[idx]
if shm.buf[0] == 0:
shm.buf[0] = 1
return idx, shm.buf[1:]
# No free SHMs. Try to create a new SHM.
# We'll start after the maximum index, but this may create a sparse list.
# Having a sparse list shouldn't really matter though.
idx = 0 if not shm_list else max(shm_list) + 1
while True:
try:
shm = shared_memory.SharedMemory(name=f"{self.name}_{idx}", create=True, size=self.size+1)
shm.buf[0] = 1
shm_list[idx] = shm
return idx, shm.buf[1:]
except FileExistsError:
# The SHM already exists. Return it if it is free.
shm = shared_memory.SharedMemory(name=f"{self.name}_{idx}")
if (shm.buf[0] == 0):
shm.buf[0] = 1
shm_list[idx] = shm
return idx, shm.buf[1:]
# It's locked, so we move to the next idx.
idx = idx + 1
""" Get an existing frame.
Args:
shape: The shape of the resulting numpy array
index: The index number of the frame to get.
If the index is not set, fetches the shm with <name>.
Returns:
A numpy array with the frame.
"""
def get(self, shape, index : Optional[int] = None, name : Optional[str] = None):
if name is None:
name = self.name
if name is None:
raise UnsupportedOperation("Cannot get without name")
# If the shm isn't already open
if name not in self.shm_lists:
self.shm_lists[name] = {}
shm_list = self.shm_lists[name]
if index is None:
index = -1 # special value for "root" index
if index not in shm_list:
# The shm isn't open yet, so open it and cache it in our list
memory_name = name if index == -1 else f"{name}_{index}"
shm = shared_memory.SharedMemory(name=memory_name)
shm_list[index] = shm
else: else:
shm = shared_memory.SharedMemory(name=name) shm = shm_list[index]
self.shm_store[name] = shm
return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
def close(self, name): buffer = shm.buf if index is -1 else shm.buf[1:]
if name in self.shm_store: return np.ndarray(shape, dtype=np.uint8, buffer=buffer)
self.shm_store[name].close()
del self.shm_store[name] """ Close the frame, freeing it for reuse.
Args:
index: The index number of the frame to free.
"""
def close(self, index : int, name : Optional[str] = None):
if name is None:
name = self.name
if name is None:
raise UnsupportedOperation("Cannot close without name")
self.shm_lists[name][index].buf[0] = 0
def delete(self, name): def delete(self, name):
### will be deprecated
if name in self.shm_store: if name in self.shm_store:
self.shm_store[name].close() self.shm_store[name].close()
self.shm_store[name].unlink() self.shm_store[name].unlink()

View File

@ -11,6 +11,7 @@ import numpy as np
import copy import copy
import itertools import itertools
import json import json
import traceback
import base64 import base64
from typing import Dict, List from typing import Dict, List
from collections import defaultdict from collections import defaultdict
@ -133,16 +134,15 @@ def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: Fram
break break
current_frame.value = datetime.datetime.now().timestamp() current_frame.value = datetime.datetime.now().timestamp()
frame_name = f"{camera_name}{current_frame.value}" #frame_name = f"{camera_name}{current_frame.value}"
frame_buffer = frame_manager.create(frame_name, frame_size) idx, frame_buffer = frame_manager.create()
try: try:
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size) frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
except: except:
print(f"{camera_name}: ffmpeg sent a broken frame. something is wrong.") print(f"{camera_name}: ffmpeg sent a broken frame. something is wrong.")
if ffmpeg_process.poll() != None: if ffmpeg_process.poll() != None:
print(f"{camera_name}: ffmpeg process is not running. exiting capture thread...") print(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
frame_manager.delete(frame_name) frame_manager.close(idx)
break break
continue continue
@ -152,20 +152,17 @@ def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: Fram
frame_num += 1 frame_num += 1
if (frame_num % take_frame) != 0: if (frame_num % take_frame) != 0:
skipped_eps.update() skipped_eps.update()
frame_manager.delete(frame_name) frame_manager.close(idx)
continue continue
# if the queue is full, skip this frame # if the queue is full, skip this frame
if frame_queue.full(): if frame_queue.full():
skipped_eps.update() skipped_eps.update()
frame_manager.delete(frame_name) frame_manager.close(idx)
continue continue
# close the frame
frame_manager.close(frame_name)
# add to the queue # add to the queue
frame_queue.put(current_frame.value) frame_queue.put((idx, current_frame.value))
class CameraWatchdog(threading.Thread): class CameraWatchdog(threading.Thread):
def __init__(self, name, config, frame_queue, camera_fps, ffmpeg_pid, stop_event): def __init__(self, name, config, frame_queue, camera_fps, ffmpeg_pid, stop_event):
@ -224,7 +221,7 @@ class CameraCapture(threading.Thread):
self.take_frame = take_frame self.take_frame = take_frame
self.fps = fps self.fps = fps
self.skipped_fps = EventsPerSecond() self.skipped_fps = EventsPerSecond()
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager(name, frame_shape[0] * frame_shape[1] * 3 // 2)
self.ffmpeg_process = ffmpeg_process self.ffmpeg_process = ffmpeg_process
self.current_frame = mp.Value('d', 0.0) self.current_frame = mp.Value('d', 0.0)
self.last_frame = 0 self.last_frame = 0
@ -279,7 +276,7 @@ def track_camera(name, config, detection_queue, result_connection, detected_obje
object_tracker = ObjectTracker(10) object_tracker = ObjectTracker(10)
frame_manager = SharedMemoryFrameManager() frame_manager = SharedMemoryFrameManager(name)
process_frames(name, frame_queue, frame_shape, frame_manager, motion_detector, object_detector, process_frames(name, frame_queue, frame_shape, frame_manager, motion_detector, object_detector,
object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, mask, stop_event) object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, mask, stop_event)
@ -335,16 +332,16 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
break break
try: try:
frame_time = frame_queue.get(True, 10) idx, frame_time = frame_queue.get(True, 10)
except queue.Empty: except queue.Empty:
continue continue
current_frame_time.value = frame_time current_frame_time.value = frame_time
frame = frame_manager.get(f"{camera_name}{frame_time}", (frame_shape[0]*3//2, frame_shape[1])) frame = frame_manager.get((frame_shape[0]*3//2, frame_shape[1]), idx)
if frame is None: if frame is None:
print(f"{camera_name}: frame {frame_time} is not in memory store.") print(f"{camera_name}: frame {frame_time} (index {idx}) is not in memory store.")
continue continue
# look for motion # look for motion
@ -419,11 +416,11 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
# add to the queue if not full # add to the queue if not full
if(detected_objects_queue.full()): if(detected_objects_queue.full()):
frame_manager.delete(f"{camera_name}{frame_time}") frame_manager.close(idx)
continue continue
else: else:
fps_tracker.update() fps_tracker.update()
fps.value = fps_tracker.eps() fps.value = fps_tracker.eps()
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects)) detected_objects_queue.put((camera_name, idx, frame_time, object_tracker.tracked_objects))
detection_fps.value = object_detector.fps.eps() detection_fps.value = object_detector.fps.eps()
frame_manager.close(f"{camera_name}{frame_time}") frame_manager.close(idx)