mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-01 16:55:21 +03:00
Initial support for YOLOv3-tiny
This commit is contained in:
parent
42410a260c
commit
012e2ca00d
@ -112,7 +112,13 @@ detectors:
|
|||||||
```yaml
|
```yaml
|
||||||
model:
|
model:
|
||||||
# Required: height of the trained model
|
# Required: height of the trained model
|
||||||
height: 320
|
height: 416
|
||||||
# Required: width of the trained model
|
# Required: width of the trained model
|
||||||
width: 320
|
width: 416
|
||||||
|
# Required: type of model (ssd or yolo)
|
||||||
|
model_type: 'yolo'
|
||||||
|
# Required: path of label map
|
||||||
|
label_path: '/labelmap.txt'
|
||||||
|
# Optional: (but required for yolo) - anchors, comma separated
|
||||||
|
anchors: '10,14, 23,27, 37,58, 81,82, 135,169, 344,319'
|
||||||
```
|
```
|
||||||
|
|||||||
@ -24,6 +24,32 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
|
|||||||
|
|
||||||
You also need to update the model width/height in the config if they differ from the defaults.
|
You also need to update the model width/height in the config if they differ from the defaults.
|
||||||
|
|
||||||
|
You can also try improving the speed using a YOLOv3-tiny model, quantized to work on the edge TPU.
|
||||||
|
|
||||||
|
A compiled model exists [here](https://github.com/guichristmann/edge-tpu-tiny-yolo/tree/master/models)
|
||||||
|
|
||||||
|
Add it as a volume mount in your docker-compose file:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /path/to/quant_coco-tiny-v3-relu_edgetpu.tflite:/edgetpu_model.tflite
|
||||||
|
```
|
||||||
|
|
||||||
|
And then set the configuration for the model in config.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
model:
|
||||||
|
# Required: height of the trained model
|
||||||
|
height: 416
|
||||||
|
# Required: width of the trained model
|
||||||
|
width: 416
|
||||||
|
# Required: type of model (ssd or yolo)
|
||||||
|
model_type: 'yolo'
|
||||||
|
# Required: path of label map
|
||||||
|
label_path: '/labelmap.txt'
|
||||||
|
# Optional: (but required for yolo) - anchors, comma separated
|
||||||
|
anchors: '10,14, 23,27, 37,58, 81,82, 135,169, 344,319'
|
||||||
|
```
|
||||||
|
|
||||||
### Customizing the Labelmap
|
### Customizing the Labelmap
|
||||||
|
|
||||||
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. You must retain the same number of labels, but you can change the names. To change:
|
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. You must retain the same number of labels, but you can change the names. To change:
|
||||||
|
|||||||
@ -31,6 +31,7 @@ from frigate.zeroconf import broadcast_zeroconf
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FrigateApp():
|
class FrigateApp():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.stop_event = mp.Event()
|
self.stop_event = mp.Event()
|
||||||
@ -153,9 +154,9 @@ class FrigateApp():
|
|||||||
|
|
||||||
for name, detector in self.config.detectors.items():
|
for name, detector in self.config.detectors.items():
|
||||||
if detector.type == 'cpu':
|
if detector.type == 'cpu':
|
||||||
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, 'cpu', detector.num_threads)
|
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, self.config.model, 'cpu', detector.num_threads)
|
||||||
if detector.type == 'edgetpu':
|
if detector.type == 'edgetpu':
|
||||||
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, detector.device, detector.num_threads)
|
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, self.config.model, detector.device, detector.num_threads)
|
||||||
|
|
||||||
def start_detected_frames_processor(self):
|
def start_detected_frames_processor(self):
|
||||||
self.detected_frames_processor = TrackedObjectProcessor(self.config, self.mqtt_client, self.config.mqtt.topic_prefix,
|
self.detected_frames_processor = TrackedObjectProcessor(self.config, self.mqtt_client, self.config.mqtt.topic_prefix,
|
||||||
|
|||||||
@ -244,9 +244,12 @@ FRIGATE_CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional('database', default={}): {
|
vol.Optional('database', default={}): {
|
||||||
vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
|
vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
|
||||||
},
|
},
|
||||||
vol.Optional('model', default={'width': 320, 'height': 320}): {
|
vol.Optional('model', default={'width': 320, 'height': 320, 'model_type': 'ssd', 'label_path': '/labelmap.txt'}): {
|
||||||
vol.Required('width'): int,
|
vol.Required('width'): int,
|
||||||
vol.Required('height'): int
|
vol.Required('height'): int,
|
||||||
|
vol.Required('model_type') : vol.In(['ssd', 'yolo']),
|
||||||
|
vol.Required('label_path') : str,
|
||||||
|
vol.Optional('anchors', default="") : str
|
||||||
},
|
},
|
||||||
vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
|
vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
|
||||||
'mqtt': MQTT_SCHEMA,
|
'mqtt': MQTT_SCHEMA,
|
||||||
@ -288,6 +291,9 @@ class ModelConfig():
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self._width = config['width']
|
self._width = config['width']
|
||||||
self._height = config['height']
|
self._height = config['height']
|
||||||
|
self._label_path = config['label_path']
|
||||||
|
self._model_type = config['model_type']
|
||||||
|
self._anchors = config['anchors']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self):
|
||||||
@ -297,10 +303,25 @@ class ModelConfig():
|
|||||||
def height(self):
|
def height(self):
|
||||||
return self._height
|
return self._height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_path(self):
|
||||||
|
return self._label_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_type(self):
|
||||||
|
return self._model_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anchors(self):
|
||||||
|
return self._anchors
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'width': self.width,
|
'width': self.width,
|
||||||
'height': self.height
|
'height': self.height,
|
||||||
|
'label_path': self.label_path,
|
||||||
|
'model_type': self.model_type,
|
||||||
|
'anchors': self.anchors
|
||||||
}
|
}
|
||||||
|
|
||||||
class DetectorConfig():
|
class DetectorConfig():
|
||||||
|
|||||||
@ -27,8 +27,10 @@ def load_labels(path, encoding='utf-8'):
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary mapping indices to labels.
|
Dictionary mapping indices to labels.
|
||||||
"""
|
"""
|
||||||
|
logger.warn(f"Loaded labels from {path}")
|
||||||
with open(path, 'r', encoding=encoding) as f:
|
with open(path, 'r', encoding=encoding) as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
if not lines:
|
if not lines:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -44,12 +46,10 @@ class ObjectDetector(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class LocalObjectDetector(ObjectDetector):
|
class LocalObjectDetector(ObjectDetector):
|
||||||
def __init__(self, tf_device=None, num_threads=3, labels=None):
|
def __init__(self, model_config, tf_device=None, num_threads=3):
|
||||||
self.fps = EventsPerSecond()
|
self.fps = EventsPerSecond()
|
||||||
if labels is None:
|
self.labels = load_labels(model_config.label_path)
|
||||||
self.labels = {}
|
self.model_config = model_config
|
||||||
else:
|
|
||||||
self.labels = load_labels(labels)
|
|
||||||
|
|
||||||
device_config = {"device": "usb"}
|
device_config = {"device": "usb"}
|
||||||
if not tf_device is None:
|
if not tf_device is None:
|
||||||
@ -77,6 +77,10 @@ class LocalObjectDetector(ObjectDetector):
|
|||||||
self.tensor_input_details = self.interpreter.get_input_details()
|
self.tensor_input_details = self.interpreter.get_input_details()
|
||||||
self.tensor_output_details = self.interpreter.get_output_details()
|
self.tensor_output_details = self.interpreter.get_output_details()
|
||||||
|
|
||||||
|
if model_config.anchors != "":
|
||||||
|
anchors = [float(x) for x in model_config.anchors.split(',')]
|
||||||
|
self.anchors = np.array(anchors).reshape(-1, 2)
|
||||||
|
|
||||||
def detect(self, tensor_input, threshold=.4):
|
def detect(self, tensor_input, threshold=.4):
|
||||||
detections = []
|
detections = []
|
||||||
|
|
||||||
@ -93,20 +97,136 @@ class LocalObjectDetector(ObjectDetector):
|
|||||||
self.fps.update()
|
self.fps.update()
|
||||||
return detections
|
return detections
|
||||||
|
|
||||||
|
def sigmoid(self, x):
|
||||||
|
return 1. / (1 + np.exp(-x))
|
||||||
|
|
||||||
def detect_raw(self, tensor_input):
|
def detect_raw(self, tensor_input):
|
||||||
|
if self.model_config.model_type == "ssd":
|
||||||
|
raw_detections = self.detect_ssd(tensor_input)
|
||||||
|
elif self.model_config.model_type == "yolo":
|
||||||
|
raw_detections = self.detect_yolo(tensor_input)
|
||||||
|
else:
|
||||||
|
logger.error(f"Unsupported model type {self.model_config.model_type}")
|
||||||
|
raw_detections = []
|
||||||
|
return raw_detections
|
||||||
|
|
||||||
|
|
||||||
|
def get_interpreter_details(self):
|
||||||
|
# Get input and output tensor details
|
||||||
|
input_details = self.interpreter.get_input_details()
|
||||||
|
output_details = self.interpreter.get_output_details()
|
||||||
|
input_shape = input_details[0]["shape"]
|
||||||
|
return input_details, output_details, input_shape
|
||||||
|
|
||||||
|
# from util.py in https://github.com/guichristmann/edge-tpu-tiny-yolo
|
||||||
|
def featuresToBoxes(self, outputs, anchors, n_classes, net_input_shape):
|
||||||
|
grid_shape = outputs.shape[1:3]
|
||||||
|
n_anchors = len(anchors)
|
||||||
|
|
||||||
|
# Numpy screwaround to get the boxes in reasonable amount of time
|
||||||
|
grid_y = np.tile(np.arange(grid_shape[0]).reshape(-1, 1), grid_shape[0]).reshape(1, grid_shape[0], grid_shape[0], 1).astype(np.float32)
|
||||||
|
grid_x = grid_y.copy().T.reshape(1, grid_shape[0], grid_shape[1], 1).astype(np.float32)
|
||||||
|
outputs = outputs.reshape(1, grid_shape[0], grid_shape[1], n_anchors, -1)
|
||||||
|
_anchors = anchors.reshape(1, 1, 3, 2).astype(np.float32)
|
||||||
|
|
||||||
|
# Get box parameters from network output and apply transformations
|
||||||
|
bx = (self.sigmoid(outputs[..., 0]) + grid_x) / grid_shape[0]
|
||||||
|
by = (self.sigmoid(outputs[..., 1]) + grid_y) / grid_shape[1]
|
||||||
|
# Should these be inverted?
|
||||||
|
bw = np.multiply(_anchors[..., 0] / net_input_shape[1], np.exp(outputs[..., 2]))
|
||||||
|
bh = np.multiply(_anchors[..., 1] / net_input_shape[2], np.exp(outputs[..., 3]))
|
||||||
|
|
||||||
|
# Get the scores
|
||||||
|
scores = self.sigmoid(np.expand_dims(outputs[..., 4], -1)) * \
|
||||||
|
self.sigmoid(outputs[..., 5:])
|
||||||
|
scores = scores.reshape(-1, n_classes)
|
||||||
|
|
||||||
|
# TODO: some of these are probably not needed but I don't understand numpy magic well enough
|
||||||
|
bx = bx.flatten()
|
||||||
|
by = (by.flatten()) * 1
|
||||||
|
bw = bw.flatten()
|
||||||
|
bh = bh.flatten() * 1
|
||||||
|
half_bw = bw / 2.
|
||||||
|
half_bh = bh / 2.
|
||||||
|
|
||||||
|
tl_x = np.multiply(bx - half_bw, 1)
|
||||||
|
tl_y = np.multiply(by - half_bh, 1)
|
||||||
|
br_x = np.multiply(bx + half_bw, 1)
|
||||||
|
br_y = np.multiply(by + half_bh, 1)
|
||||||
|
|
||||||
|
# Get indices of boxes with score higher than threshold
|
||||||
|
indices = np.argwhere(scores >= 0.5)
|
||||||
|
selected_boxes = []
|
||||||
|
selected_scores = []
|
||||||
|
for i in indices:
|
||||||
|
i = tuple(i)
|
||||||
|
selected_boxes.append( ((tl_x[i[0]], tl_y[i[0]]), (br_x[i[0]], br_y[i[0]])) )
|
||||||
|
selected_scores.append(scores[i])
|
||||||
|
|
||||||
|
selected_boxes = np.array(selected_boxes)
|
||||||
|
selected_scores = np.array(selected_scores)
|
||||||
|
selected_classes = indices[:, 1]
|
||||||
|
|
||||||
|
return selected_boxes, selected_scores, selected_classes
|
||||||
|
|
||||||
|
def detect_ssd(self, tensor_input):
|
||||||
self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
|
self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
|
||||||
self.interpreter.invoke()
|
self.interpreter.invoke()
|
||||||
boxes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[0]['index']))
|
boxes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[0]['index']))
|
||||||
label_codes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[1]['index']))
|
label_codes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[1]['index']))
|
||||||
scores = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[2]['index']))
|
scores = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[2]['index']))
|
||||||
|
|
||||||
|
# TODO: there's definitely a bug here. 20 seems to be artifically chosen and you'll get an indexing error if there's
|
||||||
|
# more than 20 results.
|
||||||
detections = np.zeros((20,6), np.float32)
|
detections = np.zeros((20,6), np.float32)
|
||||||
for i, score in enumerate(scores):
|
for i, score in enumerate(scores):
|
||||||
detections[i] = [label_codes[i], score, boxes[i][0], boxes[i][1], boxes[i][2], boxes[i][3]]
|
detections[i] = [label_codes[i], score, boxes[i][0], boxes[i][1], boxes[i][2], boxes[i][3]]
|
||||||
|
|
||||||
return detections
|
return detections
|
||||||
|
|
||||||
def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, model_shape, tf_device, num_threads):
|
def detect_yolo(self, tensor_input):
|
||||||
|
input_details, output_details, net_input_shape = \
|
||||||
|
self.get_interpreter_details()
|
||||||
|
|
||||||
|
self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
|
||||||
|
self.interpreter.invoke()
|
||||||
|
|
||||||
|
# for yolo, it's a little diffrent
|
||||||
|
out1 = self.interpreter.get_tensor(self.tensor_output_details[0]['index'])
|
||||||
|
out2 = self.interpreter.get_tensor(self.tensor_output_details[1]['index'])
|
||||||
|
|
||||||
|
# Dequantize output (tpu only)
|
||||||
|
o1_scale, o1_zero = self.tensor_output_details[0]['quantization']
|
||||||
|
out1 = (out1.astype(np.float32) - o1_zero) * o1_scale
|
||||||
|
o2_scale, o2_zero = self.tensor_output_details[1]['quantization']
|
||||||
|
out2 = (out2.astype(np.float32) - o2_zero) * o2_scale
|
||||||
|
|
||||||
|
num_classes = len(self.labels)
|
||||||
|
_boxes1, _scores1, _classes1 = self.featuresToBoxes(out1, self.anchors[[3, 4, 5]], len(self.labels), net_input_shape)
|
||||||
|
_boxes2, _scores2, _classes2 = self.featuresToBoxes(out2, self.anchors[[1, 2, 3]], len(self.labels), net_input_shape)
|
||||||
|
|
||||||
|
if _boxes1.shape[0] == 0:
|
||||||
|
_boxes1 = np.empty([0, 2, 2])
|
||||||
|
_scores1 = np.empty([0,])
|
||||||
|
_classes1 = np.empty([0,])
|
||||||
|
if _boxes2.shape[0] == 0:
|
||||||
|
_boxes2 = np.empty([0, 2, 2])
|
||||||
|
_scores2 = np.empty([0,])
|
||||||
|
_classes2 = np.empty([0,])
|
||||||
|
boxes = np.append(_boxes1, _boxes2, axis=0)
|
||||||
|
scores = np.append(_scores1, _scores2, axis=0)
|
||||||
|
label_codes = np.append(_classes1, _classes2, axis=0)
|
||||||
|
|
||||||
|
# TODO: there's definitely a bug here. 20 seems to be artifically chosen and you'll get an indexing error if there's
|
||||||
|
# more than 20 results.
|
||||||
|
detections = np.zeros((20,6), np.float32)
|
||||||
|
for i, score in enumerate(scores):
|
||||||
|
if i < 20:
|
||||||
|
detections[i] = [label_codes[i], score, boxes[i][0][1], boxes[i][0][0], boxes[i][1][1], boxes[i][1][0]]
|
||||||
|
|
||||||
|
return detections
|
||||||
|
|
||||||
|
def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, model_config, tf_device, num_threads):
|
||||||
threading.current_thread().name = f"detector:{name}"
|
threading.current_thread().name = f"detector:{name}"
|
||||||
logger = logging.getLogger(f"detector.{name}")
|
logger = logging.getLogger(f"detector.{name}")
|
||||||
logger.info(f"Starting detection process: {os.getpid()}")
|
logger.info(f"Starting detection process: {os.getpid()}")
|
||||||
@ -121,7 +241,7 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
|
|||||||
signal.signal(signal.SIGINT, receiveSignal)
|
signal.signal(signal.SIGINT, receiveSignal)
|
||||||
|
|
||||||
frame_manager = SharedMemoryFrameManager()
|
frame_manager = SharedMemoryFrameManager()
|
||||||
object_detector = LocalObjectDetector(tf_device=tf_device, num_threads=num_threads)
|
object_detector = LocalObjectDetector(model_config, tf_device=tf_device, num_threads=num_threads)
|
||||||
|
|
||||||
outputs = {}
|
outputs = {}
|
||||||
for name in out_events.keys():
|
for name in out_events.keys():
|
||||||
@ -140,7 +260,7 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
|
|||||||
connection_id = detection_queue.get(timeout=5)
|
connection_id = detection_queue.get(timeout=5)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
input_frame = frame_manager.get(connection_id, (1,model_shape[0],model_shape[1],3))
|
input_frame = frame_manager.get(connection_id, (1, model_config.height, model_config.width,3))
|
||||||
|
|
||||||
if input_frame is None:
|
if input_frame is None:
|
||||||
continue
|
continue
|
||||||
@ -156,16 +276,16 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
|
|||||||
avg_speed.value = (avg_speed.value*9 + duration)/10
|
avg_speed.value = (avg_speed.value*9 + duration)/10
|
||||||
|
|
||||||
class EdgeTPUProcess():
|
class EdgeTPUProcess():
|
||||||
def __init__(self, name, detection_queue, out_events, model_shape, tf_device=None, num_threads=3):
|
def __init__(self, name, detection_queue, out_events, model_config, tf_device=None, num_threads=3):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.out_events = out_events
|
self.out_events = out_events
|
||||||
self.detection_queue = detection_queue
|
self.detection_queue = detection_queue
|
||||||
self.avg_inference_speed = mp.Value('d', 0.01)
|
self.avg_inference_speed = mp.Value('d', 0.01)
|
||||||
self.detection_start = mp.Value('d', 0.0)
|
self.detection_start = mp.Value('d', 0.0)
|
||||||
self.detect_process = None
|
self.detect_process = None
|
||||||
self.model_shape = model_shape
|
|
||||||
self.tf_device = tf_device
|
self.tf_device = tf_device
|
||||||
self.num_threads = num_threads
|
self.num_threads = num_threads
|
||||||
|
self.model_config = model_config
|
||||||
self.start_or_restart()
|
self.start_or_restart()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -181,7 +301,7 @@ class EdgeTPUProcess():
|
|||||||
self.detection_start.value = 0.0
|
self.detection_start.value = 0.0
|
||||||
if (not self.detect_process is None) and self.detect_process.is_alive():
|
if (not self.detect_process is None) and self.detect_process.is_alive():
|
||||||
self.stop()
|
self.stop()
|
||||||
self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.model_shape, self.tf_device, self.num_threads))
|
self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.model_config, self.tf_device, self.num_threads))
|
||||||
self.detect_process.daemon = True
|
self.detect_process.daemon = True
|
||||||
self.detect_process.start()
|
self.detect_process.start()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user