Skip to content

Commit 4383b88

Browse files
NateMeyerNickM-27
andauthored
Refactor to simplify support for additional detector types (blakeblackshear#3656)
* Refactor EdgeTPU and CPU model handling to detector submodules. * Fix selecting the correct detection device type from the config * Remove detector type check when creating ObjectDetectProcess * Fixes after rebasing to 0.11 * Add init file to detector folder * Rename to detect_api Co-authored-by: Nicolas Mowen <[email protected]> * Add unit test for LocalObjectDetector class * Add configuration for model inputs Support transforming detection regions to RGB or BGR. Support specifying the input tensor shape. The tensor shape has a standard format ["BHWC"] when handed to the detector, but can be transformed in the detector to match the model shape using the model input_tensor config. * Add documentation for new model config parameters * Add input tensor transpose to LocalObjectDetector * Change the model input tensor config to use an enumeration * Updates for model config documentation Co-authored-by: Nicolas Mowen <[email protected]>
1 parent 1bc9efd commit 4383b88

17 files changed

+457
-151
lines changed

benchmark.py

+40-25
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
import multiprocessing as mp
44
import numpy as np
55
import datetime
6-
from frigate.edgetpu import LocalObjectDetector, EdgeTPUProcess, RemoteObjectDetector, load_labels
6+
from frigate.config import DetectorTypeEnum
7+
from frigate.object_detection import (
8+
LocalObjectDetector,
9+
ObjectDetectProcess,
10+
RemoteObjectDetector,
11+
load_labels,
12+
)
713

8-
my_frame = np.expand_dims(np.full((300,300,3), 1, np.uint8), axis=0)
9-
labels = load_labels('/labelmap.txt')
14+
my_frame = np.expand_dims(np.full((300, 300, 3), 1, np.uint8), axis=0)
15+
labels = load_labels("/labelmap.txt")
1016

1117
######
1218
# Minimal same process runner
@@ -39,20 +45,23 @@
3945

4046

4147
def start(id, num_detections, detection_queue, event):
42-
object_detector = RemoteObjectDetector(str(id), '/labelmap.txt', detection_queue, event)
43-
start = datetime.datetime.now().timestamp()
48+
object_detector = RemoteObjectDetector(
49+
str(id), "/labelmap.txt", detection_queue, event
50+
)
51+
start = datetime.datetime.now().timestamp()
4452

45-
frame_times = []
46-
for x in range(0, num_detections):
47-
start_frame = datetime.datetime.now().timestamp()
48-
detections = object_detector.detect(my_frame)
49-
frame_times.append(datetime.datetime.now().timestamp()-start_frame)
53+
frame_times = []
54+
for x in range(0, num_detections):
55+
start_frame = datetime.datetime.now().timestamp()
56+
detections = object_detector.detect(my_frame)
57+
frame_times.append(datetime.datetime.now().timestamp() - start_frame)
58+
59+
duration = datetime.datetime.now().timestamp() - start
60+
object_detector.cleanup()
61+
print(f"{id} - Processed for {duration:.2f} seconds.")
62+
print(f"{id} - FPS: {object_detector.fps.eps():.2f}")
63+
print(f"{id} - Average frame processing time: {mean(frame_times)*1000:.2f}ms")
5064

51-
duration = datetime.datetime.now().timestamp()-start
52-
object_detector.cleanup()
53-
print(f"{id} - Processed for {duration:.2f} seconds.")
54-
print(f"{id} - FPS: {object_detector.fps.eps():.2f}")
55-
print(f"{id} - Average frame processing time: {mean(frame_times)*1000:.2f}ms")
5665

5766
######
5867
# Separate process runner
@@ -71,23 +80,29 @@ def start(id, num_detections, detection_queue, event):
7180

7281
events = {}
7382
for x in range(0, 10):
74-
events[str(x)] = mp.Event()
83+
events[str(x)] = mp.Event()
7584
detection_queue = mp.Queue()
76-
edgetpu_process_1 = EdgeTPUProcess(detection_queue, events, 'usb:0')
77-
edgetpu_process_2 = EdgeTPUProcess(detection_queue, events, 'usb:1')
85+
edgetpu_process_1 = ObjectDetectProcess(
86+
detection_queue, events, DetectorTypeEnum.edgetpu, "usb:0"
87+
)
88+
edgetpu_process_2 = ObjectDetectProcess(
89+
detection_queue, events, DetectorTypeEnum.edgetpu, "usb:1"
90+
)
7891

7992
for x in range(0, 10):
80-
camera_process = mp.Process(target=start, args=(x, 300, detection_queue, events[str(x)]))
81-
camera_process.daemon = True
82-
camera_processes.append(camera_process)
93+
camera_process = mp.Process(
94+
target=start, args=(x, 300, detection_queue, events[str(x)])
95+
)
96+
camera_process.daemon = True
97+
camera_processes.append(camera_process)
8398

8499
start_time = datetime.datetime.now().timestamp()
85100

86101
for p in camera_processes:
87-
p.start()
102+
p.start()
88103

89104
for p in camera_processes:
90-
p.join()
105+
p.join()
91106

92-
duration = datetime.datetime.now().timestamp()-start_time
93-
print(f"Total - Processed for {duration:.2f} seconds.")
107+
duration = datetime.datetime.now().timestamp() - start_time
108+
print(f"Total - Processed for {duration:.2f} seconds.")

docs/docs/configuration/advanced.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Examples of available modules are:
2323

2424
- `frigate.app`
2525
- `frigate.mqtt`
26-
- `frigate.edgetpu`
26+
- `frigate.object_detection`
2727
- `frigate.zeroconf`
2828
- `detector.<detector_name>`
2929
- `watchdog.<camera_name>`
@@ -50,6 +50,30 @@ database:
5050

5151
If using a custom model, the width and height will need to be specified.
5252

53+
Custom models may also require different input tensor formats. The colorspace conversion supports RGB, BGR, or YUV frames to be sent to the object detector. The input tensor shape parameter is an enumeration to match what specified by the model.
54+
55+
| Tensor Dimension | Description |
56+
| :--------------: | -------------- |
57+
| N | Batch Size |
58+
| H | Model Height |
59+
| W | Model Width |
60+
| C | Color Channels |
61+
62+
| Available Input Tensor Shapes |
63+
| :---------------------------: |
64+
| "nhwc" |
65+
| "nchw" |
66+
67+
```yaml
68+
# Optional: model config
69+
model:
70+
path: /path/to/model
71+
width: 320
72+
height: 320
73+
input_tensor: "nhwc"
74+
input_pixel_format: "bgr"
75+
```
76+
5377
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. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
5478

5579
```yaml
@@ -71,6 +95,7 @@ Note that if you rename objects in the labelmap, you will also need to update yo
7195
Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, a docker volume mapping can be used to overwrite the included ffmpeg build with an ffmpeg build that works for your specific hardware setup.
7296

7397
To do this:
98+
7499
1. Download your ffmpeg build and uncompress to a folder on the host (let's use `/home/appdata/frigate/custom-ffmpeg` for this example).
75100
2. Update your docker-compose or docker CLI to include `'/home/appdata/frigate/custom-ffmpeg':'/usr/lib/btbn-ffmpeg':'ro'` in the volume mappings.
76101
3. Restart frigate and the custom version will be used if the mapping was done correctly.

docs/docs/configuration/index.md

+6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ model:
9797
width: 320
9898
# Required: Object detection model input height (default: shown below)
9999
height: 320
100+
# Optional: Object detection model input colorspace
101+
# Valid values are rgb, bgr, or yuv. (default: shown below)
102+
input_pixel_format: rgb
103+
# Optional: Object detection model input tensor format
104+
# Valid values are nhwc or nchw (default: shown below)
105+
input_tensor: "nhwc"
100106
# Optional: Label name modifications. These are merged into the standard labelmap.
101107
labelmap:
102108
2: vehicle

frigate/app.py

+12-26
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from frigate.config import DetectorTypeEnum, FrigateConfig
1717
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
18-
from frigate.edgetpu import EdgeTPUProcess
18+
from frigate.object_detection import ObjectDetectProcess
1919
from frigate.events import EventCleanup, EventProcessor
2020
from frigate.http import create_app
2121
from frigate.log import log_process, root_configurer
@@ -40,7 +40,7 @@ class FrigateApp:
4040
def __init__(self) -> None:
4141
self.stop_event: Event = mp.Event()
4242
self.detection_queue: Queue = mp.Queue()
43-
self.detectors: dict[str, EdgeTPUProcess] = {}
43+
self.detectors: dict[str, ObjectDetectProcess] = {}
4444
self.detection_out_events: dict[str, Event] = {}
4545
self.detection_shms: list[mp.shared_memory.SharedMemory] = []
4646
self.log_queue: Queue = mp.Queue()
@@ -178,8 +178,6 @@ def start_mqtt_relay(self) -> None:
178178
self.mqtt_relay.start()
179179

180180
def start_detectors(self) -> None:
181-
model_path = self.config.model.path
182-
model_shape = (self.config.model.height, self.config.model.width)
183181
for name in self.config.cameras.keys():
184182
self.detection_out_events[name] = mp.Event()
185183

@@ -203,26 +201,15 @@ def start_detectors(self) -> None:
203201
self.detection_shms.append(shm_out)
204202

205203
for name, detector in self.config.detectors.items():
206-
if detector.type == DetectorTypeEnum.cpu:
207-
self.detectors[name] = EdgeTPUProcess(
208-
name,
209-
self.detection_queue,
210-
self.detection_out_events,
211-
model_path,
212-
model_shape,
213-
"cpu",
214-
detector.num_threads,
215-
)
216-
if detector.type == DetectorTypeEnum.edgetpu:
217-
self.detectors[name] = EdgeTPUProcess(
218-
name,
219-
self.detection_queue,
220-
self.detection_out_events,
221-
model_path,
222-
model_shape,
223-
detector.device,
224-
detector.num_threads,
225-
)
204+
self.detectors[name] = ObjectDetectProcess(
205+
name,
206+
self.detection_queue,
207+
self.detection_out_events,
208+
self.config.model,
209+
detector.type,
210+
detector.device,
211+
detector.num_threads,
212+
)
226213

227214
def start_detected_frames_processor(self) -> None:
228215
self.detected_frames_processor = TrackedObjectProcessor(
@@ -253,7 +240,6 @@ def start_video_output_processor(self) -> None:
253240
logger.info(f"Output process started: {output_processor.pid}")
254241

255242
def start_camera_processors(self) -> None:
256-
model_shape = (self.config.model.height, self.config.model.width)
257243
for name, config in self.config.cameras.items():
258244
if not self.config.cameras[name].enabled:
259245
logger.info(f"Camera processor not started for disabled camera {name}")
@@ -265,7 +251,7 @@ def start_camera_processors(self) -> None:
265251
args=(
266252
name,
267253
config,
268-
model_shape,
254+
self.config.model,
269255
self.config.model.merged_labelmap,
270256
self.detection_queue,
271257
self.detection_out_events[name],

frigate/config.py

+17
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,17 @@ class DatabaseConfig(FrigateBaseModel):
718718
)
719719

720720

721+
class PixelFormatEnum(str, Enum):
722+
rgb = "rgb"
723+
bgr = "bgr"
724+
yuv = "yuv"
725+
726+
727+
class InputTensorEnum(str, Enum):
728+
nchw = "nchw"
729+
nhwc = "nhwc"
730+
731+
721732
class ModelConfig(FrigateBaseModel):
722733
path: Optional[str] = Field(title="Custom Object detection model path.")
723734
labelmap_path: Optional[str] = Field(title="Label map for custom object detector.")
@@ -726,6 +737,12 @@ class ModelConfig(FrigateBaseModel):
726737
labelmap: Dict[int, str] = Field(
727738
default_factory=dict, title="Labelmap customization."
728739
)
740+
input_tensor: InputTensorEnum = Field(
741+
default=InputTensorEnum.nhwc, title="Model Input Tensor Shape"
742+
)
743+
input_pixel_format: PixelFormatEnum = Field(
744+
default=PixelFormatEnum.rgb, title="Model Input Pixel Color Format"
745+
)
729746
_merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
730747
_colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
731748

frigate/detectors/__init__.py

Whitespace-only changes.

frigate/detectors/cpu_tfl.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import logging
2+
import numpy as np
3+
4+
from frigate.detectors.detection_api import DetectionApi
5+
import tflite_runtime.interpreter as tflite
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class CpuTfl(DetectionApi):
11+
def __init__(self, det_device=None, model_config=None, num_threads=3):
12+
self.interpreter = tflite.Interpreter(
13+
model_path=model_config.path or "/cpu_model.tflite", num_threads=num_threads
14+
)
15+
16+
self.interpreter.allocate_tensors()
17+
18+
self.tensor_input_details = self.interpreter.get_input_details()
19+
self.tensor_output_details = self.interpreter.get_output_details()
20+
21+
def detect_raw(self, tensor_input):
22+
self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input)
23+
self.interpreter.invoke()
24+
25+
boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0]
26+
class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0]
27+
scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0]
28+
count = int(
29+
self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]
30+
)
31+
32+
detections = np.zeros((20, 6), np.float32)
33+
34+
for i in range(count):
35+
if scores[i] < 0.4 or i == 20:
36+
break
37+
detections[i] = [
38+
class_ids[i],
39+
float(scores[i]),
40+
boxes[i][0],
41+
boxes[i][1],
42+
boxes[i][2],
43+
boxes[i][3],
44+
]
45+
46+
return detections

frigate/detectors/detection_api.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Dict
5+
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class DetectionApi(ABC):
11+
@abstractmethod
12+
def __init__(self, det_device=None, model_config=None):
13+
pass
14+
15+
@abstractmethod
16+
def detect_raw(self, tensor_input):
17+
pass

frigate/detectors/edgetpu_tfl.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import logging
2+
import numpy as np
3+
4+
from frigate.detectors.detection_api import DetectionApi
5+
import tflite_runtime.interpreter as tflite
6+
from tflite_runtime.interpreter import load_delegate
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class EdgeTpuTfl(DetectionApi):
12+
def __init__(self, det_device=None, model_config=None):
13+
device_config = {"device": "usb"}
14+
if not det_device is None:
15+
device_config = {"device": det_device}
16+
17+
edge_tpu_delegate = None
18+
19+
try:
20+
logger.info(f"Attempting to load TPU as {device_config['device']}")
21+
edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config)
22+
logger.info("TPU found")
23+
self.interpreter = tflite.Interpreter(
24+
model_path=model_config.path or "/edgetpu_model.tflite",
25+
experimental_delegates=[edge_tpu_delegate],
26+
)
27+
except ValueError:
28+
logger.error(
29+
"No EdgeTPU was detected. If you do not have a Coral device yet, you must configure CPU detectors."
30+
)
31+
raise
32+
33+
self.interpreter.allocate_tensors()
34+
35+
self.tensor_input_details = self.interpreter.get_input_details()
36+
self.tensor_output_details = self.interpreter.get_output_details()
37+
38+
def detect_raw(self, tensor_input):
39+
self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input)
40+
self.interpreter.invoke()
41+
42+
boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0]
43+
class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0]
44+
scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0]
45+
count = int(
46+
self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]
47+
)
48+
49+
detections = np.zeros((20, 6), np.float32)
50+
51+
for i in range(count):
52+
if scores[i] < 0.4 or i == 20:
53+
break
54+
detections[i] = [
55+
class_ids[i],
56+
float(scores[i]),
57+
boxes[i][0],
58+
boxes[i][1],
59+
boxes[i][2],
60+
boxes[i][3],
61+
]
62+
63+
return detections

0 commit comments

Comments
 (0)