forked from Github/frigate
Compare commits
41 Commits
v0.6.0-rc1
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309c0dcda3 | ||
|
|
b35cc01035 | ||
|
|
6e79a5402e | ||
|
|
a989f8daaf | ||
|
|
7880d24b29 | ||
|
|
fdc8bbf72d | ||
|
|
005e188d38 | ||
|
|
adcc3e9b98 | ||
|
|
5fe201da25 | ||
|
|
974f7bd0df | ||
|
|
780ae7cd4f | ||
|
|
50e568b84c | ||
|
|
1ce993051e | ||
|
|
69406343ee | ||
|
|
1c33b8acb2 | ||
|
|
5e77436d39 | ||
|
|
e26308a05b | ||
|
|
c16ee3186f | ||
|
|
fedeeab561 | ||
|
|
bfcaabecfa | ||
|
|
606fa6f6d5 | ||
|
|
6a8d8bf53d | ||
|
|
1f81cba706 | ||
|
|
5db7b242aa | ||
|
|
0b7f65e227 | ||
|
|
2f758af097 | ||
|
|
f64320a464 | ||
|
|
3e87ef6426 | ||
|
|
acb75fa02d | ||
|
|
ea4ecae27c | ||
|
|
a8556a729b | ||
|
|
068df3ef2d | ||
|
|
b304139db2 | ||
|
|
df2aae5169 | ||
|
|
351ac4ec7d | ||
|
|
12e40291c0 | ||
|
|
8af7d51159 | ||
|
|
84ada716ac | ||
|
|
cbcc89be9c | ||
|
|
73a5e11b9b | ||
|
|
194baaeb56 |
55
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
55
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Version of frigate**
|
||||
What version are you using?
|
||||
|
||||
**Config file**
|
||||
Include your full config file wrapped in back ticks.
|
||||
```
|
||||
config here
|
||||
```
|
||||
|
||||
**Logs**
|
||||
```
|
||||
Include relevant log output here
|
||||
```
|
||||
|
||||
**Frigate debug stats**
|
||||
```
|
||||
Output from frigate's /debug/stats endpoint
|
||||
```
|
||||
|
||||
**FFprobe from your camera**
|
||||
|
||||
Run the following command and paste output below
|
||||
```
|
||||
ffprobe <stream_url>
|
||||
```
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Computer Hardware**
|
||||
- OS: [e.g. Ubuntu, Windows]
|
||||
- Virtualization: [e.g. Proxmox, Virtualbox]
|
||||
- Coral Version: [e.g. USB, PCIe, None]
|
||||
- Network Setup: [e.g. Wired, WiFi]
|
||||
|
||||
**Camera Info:**
|
||||
- Manufacturer: [e.g. Dahua]
|
||||
- Model: [e.g. IPC-HDW5231R-ZE]
|
||||
- Resolution: [e.g. 720p]
|
||||
- FPS: [e.g. 5]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -17,6 +17,7 @@ RUN apt -qq update && apt -qq install --no-install-recommends -y \
|
||||
ffmpeg \
|
||||
# VAAPI drivers for Intel hardware accel
|
||||
libva-drm2 libva2 i965-va-driver vainfo \
|
||||
&& python3.7 -m pip install -U pip \
|
||||
&& python3.7 -m pip install -U wheel setuptools \
|
||||
&& python3.7 -m pip install -U \
|
||||
opencv-python-headless \
|
||||
@@ -31,6 +32,7 @@ RUN apt -qq update && apt -qq install --no-install-recommends -y \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
pyarrow \
|
||||
click \
|
||||
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||
&& wget -q -O - https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - \
|
||||
&& apt -qq update \
|
||||
@@ -46,7 +48,7 @@ RUN apt -qq update && apt -qq install --no-install-recommends -y \
|
||||
|
||||
# get model and labels
|
||||
RUN wget -q https://github.com/google-coral/edgetpu/raw/master/test_data/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite -O /edgetpu_model.tflite --trust-server-names
|
||||
RUN wget -q https://dl.google.com/coral/canned_models/coco_labels.txt -O /labelmap.txt --trust-server-names
|
||||
COPY labelmap.txt /labelmap.txt
|
||||
RUN wget -q https://github.com/google-coral/edgetpu/raw/master/test_data/ssd_mobilenet_v2_coco_quant_postprocess.tflite -O /cpu_model.tflite
|
||||
|
||||
|
||||
@@ -56,5 +58,6 @@ WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
COPY detect_objects.py .
|
||||
COPY benchmark.py .
|
||||
COPY process_clip.py .
|
||||
|
||||
CMD ["python3.7", "-u", "detect_objects.py"]
|
||||
|
||||
236
README.md
236
README.md
@@ -1,7 +1,7 @@
|
||||
# Frigate - Realtime Object Detection for IP Cameras
|
||||
# Frigate - NVR With Realtime Object Detection for IP Cameras
|
||||
Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras. Designed for integration with HomeAssistant or others via MQTT.
|
||||
|
||||
Use of a [Google Coral USB Accelerator](https://coral.withgoogle.com/products/accelerator/) is optional, but highly recommended. On my Intel i7 processor, I can process 2-3 FPS with the CPU. The Coral can process 100+ FPS with very low CPU load.
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but highly recommended. On my Intel i7 processor, I can process 2-3 FPS with the CPU. The Coral can process 100+ FPS with very low CPU load.
|
||||
|
||||
- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame
|
||||
- Uses a very low overhead motion detection to determine where to run object detection
|
||||
@@ -19,7 +19,7 @@ You see multiple bounding boxes because it draws bounding boxes from all frames
|
||||
Run the container with
|
||||
```bash
|
||||
docker run --rm \
|
||||
-name frigate \
|
||||
-name blakeblackshear/frigate:stable \
|
||||
--privileged \
|
||||
--shm-size=512m \ # should work for a 2-3 cameras
|
||||
-v /dev/bus/usb:/dev/bus/usb \
|
||||
@@ -52,11 +52,13 @@ Example docker-compose:
|
||||
A `config.yml` file must exist in the `config` directory. See example [here](config/config.example.yml) and device specific info can be found [here](docs/DEVICES.md).
|
||||
|
||||
## Recommended Hardware
|
||||
**Note: I may receive commissions for purchases made through links below.**
|
||||
|Name|Inference Speed|Notes|
|
||||
|----|---------------|-----|
|
||||
|Atomic Pi|16ms|Best option for a dedicated low power board with a small number of cameras.|
|
||||
|Intel NUC NUC7i3BNK|8-10ms|Best possible performance. Can handle 7+ cameras at 5fps depending on typical amounts of motion.|
|
||||
|BMAX B2 Plus|10-12ms|Good balance of performance and cost. Also capable of running many other services at the same time as frigate.
|
||||
|[Atomic Pi](https://amzn.to/2FKJHpu)|16ms|Best option for a dedicated low power board with a small number of cameras.|
|
||||
|[Intel NUC NUC7i3BNK](https://amzn.to/2RDYZPe)|8-10ms|Best possible performance. Can handle 7+ cameras at 5fps depending on typical amounts of motion.|
|
||||
|[BMAX B2 Plus](https://amzn.to/3cjgQ81)|10-12ms|Good balance of performance and cost. Also capable of running many other services at the same time as frigate.|
|
||||
|[Minisforum GK41](https://amzn.to/32FyKhG)|9-10ms|Great alternative to a NUC. Easily handiles 4 1080p cameras.|
|
||||
|
||||
ARM boards are not officially supported at the moment due to some python dependencies that require modification to work on ARM devices. The Raspberry Pi4 gets about 16ms inference speeds, but the hardware acceleration for ffmpeg does not work for converting yuv420 to rgb24. The Atomic Pi is x86 and much more efficient.
|
||||
|
||||
@@ -64,7 +66,7 @@ Users have reported varying success in getting frigate to run in a VM. In some c
|
||||
|
||||
## Integration with HomeAssistant
|
||||
|
||||
Setup a the camera, binary_sensor, sensor and optionally automation as shown for each camera you define in frigate. Replace <camera_name> with the camera name as defined in the frigate `config.yml` (The `frigate_coral_fps` and `frigate_coral_inference` sensors only need to be defined once)
|
||||
Setup a camera, binary_sensor, sensor and optionally automation as shown for each camera you define in frigate. Replace <camera_name> with the camera name as defined in the frigate `config.yml` (The `frigate_coral_fps` and `frigate_coral_inference` sensors only need to be defined once)
|
||||
|
||||
```
|
||||
camera:
|
||||
@@ -137,14 +139,21 @@ An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging
|
||||
|
||||
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/back?fps=10` or both with `?fps=10&h=1000`
|
||||
|
||||
### `/<camera_name>/<object_name>/best.jpg`
|
||||
The best snapshot for any object type. It is a full resolution image by default. You can change the size of the image by appending `h=height-in-pixels` to the endpoint.
|
||||
### `/<camera_name>/<object_name>/best.jpg[?h=300&crop=1]`
|
||||
The best snapshot for any object type. It is a full resolution image by default.
|
||||
|
||||
### `/<camera_name>/latest.jpg`
|
||||
The most recent frame that frigate has finished processing. It is a full resolution image by default. You can change the size of the image by appending `h=height-in-pixels` to the endpoint.
|
||||
Example parameters:
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
|
||||
|
||||
### `/<camera_name>/latest.jpg[?h=300]`
|
||||
The most recent frame that frigate has finished processing. It is a full resolution image by default.
|
||||
|
||||
Example parameters:
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
|
||||
### `/debug/stats`
|
||||
Contains some granular debug info that can be used for sensors in HomeAssistant.
|
||||
Contains some granular debug info that can be used for sensors in HomeAssistant. See details below.
|
||||
|
||||
## MQTT Messages
|
||||
These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file.
|
||||
@@ -161,55 +170,41 @@ Publishes `ON` or `OFF` and is designed to be used a as a binary sensor in HomeA
|
||||
Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image
|
||||
is published again.
|
||||
|
||||
The height and crop of snapshots can be configured as shown in the example config.
|
||||
|
||||
### frigate/<camera_name>/events/start
|
||||
Message published at the start of any tracked object. JSON looks as follows:
|
||||
```json
|
||||
{
|
||||
"label": "person",
|
||||
"score": 0.7890625,
|
||||
"score": 0.87890625,
|
||||
"box": [
|
||||
468,
|
||||
446,
|
||||
550,
|
||||
592
|
||||
95,
|
||||
155,
|
||||
581,
|
||||
1182
|
||||
],
|
||||
"area": 11972,
|
||||
"area": 499122,
|
||||
"region": [
|
||||
403,
|
||||
395,
|
||||
613,
|
||||
605
|
||||
0,
|
||||
132,
|
||||
1080,
|
||||
1212
|
||||
],
|
||||
"frame_time": 1594298020.819046,
|
||||
"frame_time": 1600208805.60284,
|
||||
"centroid": [
|
||||
509,
|
||||
519
|
||||
338,
|
||||
668
|
||||
],
|
||||
"id": "1594298020.819046-0",
|
||||
"start_time": 1594298020.819046,
|
||||
"top_score": 0.7890625,
|
||||
"history": [
|
||||
{
|
||||
"score": 0.7890625,
|
||||
"box": [
|
||||
468,
|
||||
446,
|
||||
550,
|
||||
592
|
||||
],
|
||||
"region": [
|
||||
403,
|
||||
395,
|
||||
613,
|
||||
605
|
||||
],
|
||||
"centroid": [
|
||||
509,
|
||||
519
|
||||
],
|
||||
"frame_time": 1594298020.819046
|
||||
}
|
||||
]
|
||||
"id": "1600208805.60284-k1l43p",
|
||||
"start_time": 1600208805.60284,
|
||||
"top_score": 0.87890625,
|
||||
"zones": [],
|
||||
"score_history": [
|
||||
0.87890625
|
||||
],
|
||||
"computed_score": 0.0,
|
||||
"false_positive": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -219,6 +214,20 @@ Same as `frigate/<camera_name>/events/start`, but with an `end_time` property as
|
||||
### frigate/<zone_name>/<object_name>
|
||||
Publishes `ON` or `OFF` and is designed to be used a as a binary sensor in HomeAssistant for whether or not that object type is detected in the zone.
|
||||
|
||||
## Understanding min_score and threshold
|
||||
`min_score` defines the minimum score for Frigate to begin tracking a detected object. Any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:
|
||||
|
||||
| Frame | Current Score | Score History | Computed Score | Detected Object |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No
|
||||
| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No
|
||||
| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No
|
||||
| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes
|
||||
| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes
|
||||
| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes
|
||||
|
||||
In frame 2, the score is below the `min_score` value, so frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example.
|
||||
|
||||
## Using a custom model or labels
|
||||
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
|
||||
- CPU Model: `/cpu_model.tflite`
|
||||
@@ -235,12 +244,34 @@ The labelmap can be customized to your needs. A common reason to do this is to c
|
||||
-v ./config/labelmap.txt:/labelmap.txt
|
||||
```
|
||||
|
||||
## Masks and limiting detection to a certain area
|
||||
You can create a *bitmap (bmp)* file the same aspect ratio as your camera feed to limit detection to certain areas. The mask works by looking at the bottom center of any bounding box (first image, red dot below) and comparing that to your mask. If that red dot falls on an area of your mask that is black, the detection (and motion) will be ignored. The mask in the second image would limit detection on this camera to only objects that are in the front yard and not the street.
|
||||
## Recording Clips
|
||||
**Note**: Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
|
||||
|
||||
<img src="docs/example-mask-check-point.png" height="300">
|
||||
<img src="docs/example-mask.bmp" height="300">
|
||||
<img src="docs/example-mask-overlay.png" height="300">
|
||||
Frigate can save video clips without any CPU overhead for encoding by simply copying the stream directly with FFmpeg. It leverages FFmpeg's segment functionality to maintain a cache of 90 seconds of video for each camera. The cache files are written to disk at /cache and do not introduce memory overhead. When an object is being tracked, it will extend the cache to ensure it can assemble a clip when the event ends. Once the event ends, it again uses FFmpeg to assemble a clip by combining the video clips without any encoding by the CPU. Assembled clips are are saved to the /clips directory along with a json file containing the current information about the tracked object.
|
||||
|
||||
### Global Configuration Options
|
||||
- `max_seconds`: This limits the size of the cache when an object is being tracked. If an object is stationary and being tracked for a long time, the cache files will expire and this value will be the maximum clip length for the *end* of the event. For example, if this is set to 300 seconds and an object is being tracked for 600 seconds, the clip will end up being the last 300 seconds. Defaults to 300 seconds.
|
||||
|
||||
### Per-camera Configuration Options
|
||||
- `pre_capture`: Defines how much time should be included in the clip prior to the beginning of the event. Defaults to 30 seconds.
|
||||
- `objects`: List of object types to save clips for. Object types here must be listed for tracking at the camera or global configuration. Defaults to all tracked objects.
|
||||
|
||||
## Google Coral Configuration
|
||||
Frigate attempts to detect your Coral device automatically. If you have multiple Coral devices or a version that is not detected automatically, you can specify using the `tensorflow_device` config option.
|
||||
|
||||
## Masks and limiting detection to a certain area
|
||||
The mask works by looking at the bottom center of any bounding box (first image, red dot below) and comparing that to your mask. If that red dot falls on an area of your mask that is black, the detection (and motion) will be ignored. The mask in the second image would limit detection on this camera to only objects that are in the front yard and not the street.
|
||||
|
||||
<a href="docs/example-mask-check-point.png"><img src="docs/example-mask-check-point.png" height="300"></a>
|
||||
<a href="docs/example-mask.bmp"><img src="docs/example-mask.bmp" height="300"></a>
|
||||
<a href="docs/example-mask-overlay.png"><img src="docs/example-mask-overlay.png" height="300"></a>
|
||||
|
||||
The following types of masks are supported:
|
||||
- `base64`: Base64 encoded image file
|
||||
- `poly`: List of x,y points like zone configuration
|
||||
- `image`: Path to an image file in the config directory
|
||||
|
||||
`base64` and `image` masks must be the same aspect ratio as your camera.
|
||||
|
||||
## Zones
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area. See the sample config for details on how to configure.
|
||||
@@ -249,6 +280,87 @@ During testing, `draw_zones` can be set in the config to tell frigate to draw th
|
||||
|
||||

|
||||
|
||||
## Debug Info
|
||||
```jsonc
|
||||
{
|
||||
/* Per Camera Stats */
|
||||
"back": {
|
||||
/***************
|
||||
* Frames per second being consumed from your camera. If this is higher
|
||||
* than it is supposed to be, you should set -r FPS in your input_args.
|
||||
* camera_fps = process_fps + skipped_fps
|
||||
***************/
|
||||
"camera_fps": 5.0,
|
||||
/***************
|
||||
* Number of times detection is run per second. This can be higher than
|
||||
* your camera FPS because frigate often looks at the same frame multiple times
|
||||
* or in multiple locations
|
||||
***************/
|
||||
"detection_fps": 1.5,
|
||||
/***************
|
||||
* PID for the ffmpeg process that consumes this camera
|
||||
***************/
|
||||
"ffmpeg_pid": 27,
|
||||
/***************
|
||||
* Timestamps of frames in various parts of processing
|
||||
***************/
|
||||
"frame_info": {
|
||||
/***************
|
||||
* Timestamp of the frame frigate is running object detection on.
|
||||
***************/
|
||||
"detect": 1596994991.91426,
|
||||
/***************
|
||||
* Timestamp of the frame frigate is processing detected objects on.
|
||||
* This is where MQTT messages are sent, zones are checked, etc.
|
||||
***************/
|
||||
"process": 1596994991.91426,
|
||||
/***************
|
||||
* Timestamp of the frame frigate last read from ffmpeg.
|
||||
***************/
|
||||
"read": 1596994991.91426
|
||||
},
|
||||
/***************
|
||||
* PID for the process that runs detection for this camera
|
||||
***************/
|
||||
"pid": 34,
|
||||
/***************
|
||||
* Frames per second being processed by frigate.
|
||||
***************/
|
||||
"process_fps": 5.1,
|
||||
/***************
|
||||
* Timestamp when the detection process started looking for a frame. If this value stays constant
|
||||
* for a long time, that means there aren't any frames in the frame queue.
|
||||
***************/
|
||||
"read_start": 1596994991.943814,
|
||||
/***************
|
||||
* Frames per second skip for processing by frigate.
|
||||
***************/
|
||||
"skipped_fps": 0.0
|
||||
},
|
||||
/* Coral Stats */
|
||||
"coral": {
|
||||
/***************
|
||||
* Timestamp when object detection started. If this value stays non-zero and constant
|
||||
* for a long time, that means the detection process is stuck.
|
||||
***************/
|
||||
"detection_start": 0.0,
|
||||
/***************
|
||||
* Frames per second of the Coral. This should be the sum of all detection_fps values from cameras.
|
||||
***************/
|
||||
"fps": 6.9,
|
||||
/***************
|
||||
* Time spent running object detection in milliseconds.
|
||||
***************/
|
||||
"inference_speed": 10.48,
|
||||
/***************
|
||||
* PID for the shared process that runs object detection on the Coral.
|
||||
***************/
|
||||
"pid": 25321
|
||||
},
|
||||
"plasma_store_rc": null // Return code for the plasma store. This should be null normally.
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
- Lower the framerate of the video feed on the camera to reduce the CPU usage for capturing the feed. Not as effective, but you can also modify the `take_frame` [configuration](config/config.example.yml) for each camera to only analyze every other frame, or every third frame, etc.
|
||||
- Hard code the resolution of each camera in your config if you are having difficulty starting frigate or if the initial ffprobe for camerea resolution fails or returns incorrect info. Example:
|
||||
@@ -261,5 +373,17 @@ cameras:
|
||||
width: 1920
|
||||
```
|
||||
- Additional logging is available in the docker container - You can view the logs by running `docker logs -t frigate`
|
||||
- Object configuration - Tracked objects types, sizes and thresholds can be defined globally and/or on a per camera basis. The global and camera object configuration is *merged*. For example, if you defined tracking person, car, and truck globally but modified your backyard camera to only track person, the global config would merge making the effective list for the backyard camera still contain person, car and truck. If you want precise object tracking per camera, best practice to put a minimal list of objects at the global level and expand objects on a per camera basis. Object threshold and area configuration will be used first from the camera object config (if defined) and then from the global config. See the [example config](config/config.example.yml) for more information.
|
||||
- Object configuration - Tracked objects types, sizes and thresholds can be defined globally and/or on a per camera basis. The global and camera object configuration is *merged*. For example, if you defined tracking person, car, and truck globally but modified your backyard camera to only track person, the global config would merge making the effective list for the backyard camera still contain person, car and truck. If you want precise object tracking per camera, best practice to put a minimal list of objects at the global level and expand objects on a per camera basis. Object threshold and area configuration will be used first from the camera object config (if defined) and then from the global config. See the [example config](config/config.example.yml) for more information.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ffmpeg didnt return a frame. something is wrong"
|
||||
Turn on logging for the camera by overriding the global_args and setting the log level to `info`:
|
||||
```yaml
|
||||
ffmpeg:
|
||||
global_args:
|
||||
- -hide_banner
|
||||
- -loglevel
|
||||
- info
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from statistics import mean
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
import datetime
|
||||
from frigate.edgetpu import ObjectDetector, EdgeTPUProcess, RemoteObjectDetector, load_labels
|
||||
from frigate.edgetpu import LocalObjectDetector, EdgeTPUProcess, RemoteObjectDetector, load_labels
|
||||
|
||||
my_frame = np.expand_dims(np.full((300,300,3), 1, np.uint8), axis=0)
|
||||
labels = load_labels('/labelmap.txt')
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
web_port: 5000
|
||||
|
||||
################
|
||||
## Tell frigate to look for a specific EdgeTPU device. Useful if you want to run multiple instances of frigate
|
||||
## on the same machine with multiple EdgeTPUs. https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
|
||||
################
|
||||
tensorflow_device: usb
|
||||
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
topic_prefix: frigate
|
||||
@@ -11,6 +17,17 @@ mqtt:
|
||||
#################
|
||||
# password: password # Optional
|
||||
|
||||
################
|
||||
# Global configuration for saving clips
|
||||
################
|
||||
save_clips:
|
||||
###########
|
||||
# Maximum length of time to retain video during long events.
|
||||
# If an object is being tracked for longer than this amount of time, the cache
|
||||
# will begin to expire and the resulting clip will be the last x seconds of the event.
|
||||
###########
|
||||
max_seconds: 300
|
||||
|
||||
#################
|
||||
# Default ffmpeg args. Optional and can be overwritten per camera.
|
||||
# Should work with most RTSP cameras that send h264 video
|
||||
@@ -53,9 +70,10 @@ mqtt:
|
||||
# unless overridden at the camera levels.
|
||||
# Keys must be valid labels. By default, the model uses coco (https://dl.google.com/coral/canned_models/coco_labels.txt).
|
||||
# All labels from the model are reported over MQTT. These values are used to filter out false positives.
|
||||
# min_area (optional): minimum width*height of the bounding box for the detected person
|
||||
# max_area (optional): maximum width*height of the bounding box for the detected person
|
||||
# threshold (optional): The minimum decimal percentage (50% hit = 0.5) for the confidence from tensorflow
|
||||
# min_area (optional): minimum width*height of the bounding box for the detected object
|
||||
# max_area (optional): maximum width*height of the bounding box for the detected object
|
||||
# min_score (optional): minimum score for the object to initiate tracking
|
||||
# threshold (optional): The minimum decimal percentage for tracked object's computed score to considered a true positive
|
||||
####################
|
||||
objects:
|
||||
track:
|
||||
@@ -66,42 +84,8 @@ objects:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.8
|
||||
|
||||
zones:
|
||||
#################
|
||||
# Name of the zone
|
||||
################
|
||||
front_steps:
|
||||
cameras:
|
||||
front_door:
|
||||
####################
|
||||
# For each camera, a list of x,y coordinates to define the polygon of the zone. The top
|
||||
# left corner is 0,0. Can also be a comma separated string of all x,y coordinates combined.
|
||||
# The same zone can exist across multiple cameras if they have overlapping FOVs.
|
||||
# An object is determined to be in the zone based on whether or not the bottom center
|
||||
# of it's bounding box is within the polygon. The polygon must have at least 3 points.
|
||||
# Coordinates can be generated at https://www.image-map.net/
|
||||
####################
|
||||
coordinates:
|
||||
- 545,1077
|
||||
- 747,939
|
||||
- 788,805
|
||||
################
|
||||
# Zone level object filters. These are applied in addition to the global and camera filters
|
||||
# and should be more restrictive than the global and camera filters. The global and camera
|
||||
# filters are applied upstream.
|
||||
################
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.8
|
||||
driveway:
|
||||
cameras:
|
||||
front_door:
|
||||
coordinates: 545,1077,747,939,788,805
|
||||
yard:
|
||||
min_score: 0.5
|
||||
threshold: 0.85
|
||||
|
||||
cameras:
|
||||
back:
|
||||
@@ -126,9 +110,18 @@ cameras:
|
||||
# width: 720
|
||||
|
||||
################
|
||||
## Optional mask. Must be the same aspect ratio as your video feed. Value is either the
|
||||
## name of a file in the config directory or a base64 encoded bmp image prefixed with
|
||||
## 'base64,' eg. 'base64,asfasdfasdf....'.
|
||||
## Specify the framerate of your camera
|
||||
##
|
||||
## NOTE: This should only be set in the event ffmpeg is unable to determine your camera's framerate
|
||||
## on its own and the reported framerate for your camera in frigate is well over what is expected.
|
||||
################
|
||||
# fps: 5
|
||||
|
||||
################
|
||||
## Optional mask. Must be the same aspect ratio as your video feed. Value is any of the following:
|
||||
## - name of a file in the config directory
|
||||
## - base64 encoded image prefixed with 'base64,' eg. 'base64,asfasdfasdf....'
|
||||
## - polygon of x,y coordinates prefixed with 'poly,' eg. 'poly,0,900,1080,900,1080,1920,0,1920'
|
||||
##
|
||||
## The mask works by looking at the bottom center of the bounding box for the detected
|
||||
## person in the image. If that pixel in the mask is a black pixel, it ignores it as a
|
||||
@@ -147,20 +140,60 @@ cameras:
|
||||
################
|
||||
take_frame: 1
|
||||
|
||||
################
|
||||
# The number of seconds to retain the highest scoring image for the best.jpg endpoint before allowing it
|
||||
# to be replaced by a newer image. Defaults to 60 seconds.
|
||||
################
|
||||
best_image_timeout: 60
|
||||
|
||||
################
|
||||
# MQTT settings
|
||||
################
|
||||
# mqtt:
|
||||
# crop_to_region: True
|
||||
# snapshot_height: 300
|
||||
|
||||
################
|
||||
# Zones
|
||||
################
|
||||
zones:
|
||||
#################
|
||||
# Name of the zone
|
||||
################
|
||||
front_steps:
|
||||
####################
|
||||
# A list of x,y coordinates to define the polygon of the zone. The top
|
||||
# left corner is 0,0. Can also be a comma separated string of all x,y coordinates combined.
|
||||
# The same zone name can exist across multiple cameras if they have overlapping FOVs.
|
||||
# An object is determined to be in the zone based on whether or not the bottom center
|
||||
# of it's bounding box is within the polygon. The polygon must have at least 3 points.
|
||||
# Coordinates can be generated at https://www.image-map.net/
|
||||
####################
|
||||
coordinates:
|
||||
- 545,1077
|
||||
- 747,939
|
||||
- 788,805
|
||||
################
|
||||
# Zone level object filters. These are applied in addition to the global and camera filters
|
||||
# and should be more restrictive than the global and camera filters. The global and camera
|
||||
# filters are applied upstream.
|
||||
################
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.8
|
||||
|
||||
################
|
||||
# This will save a clip for each tracked object by frigate along with a json file that contains
|
||||
# data related to the tracked object. This works by telling ffmpeg to write video segments to /cache
|
||||
# from the video stream without re-encoding. Clips are then created by using ffmpeg to merge segments
|
||||
# without re-encoding. The segments saved are unaltered from what frigate receives to avoid re-encoding.
|
||||
# They do not contain bounding boxes. 30 seconds of video is added to the start of the clip. These are
|
||||
# optimized to capture "false_positive" examples for improving frigate.
|
||||
# They do not contain bounding boxes. These are optimized to capture "false_positive" examples for improving frigate.
|
||||
#
|
||||
# NOTE: This will only work for camera feeds that can be copied into the mp4 container format without
|
||||
# encoding such as h264. I do not expect this to work for mjpeg streams, and it may not work for many other
|
||||
# types of streams.
|
||||
#
|
||||
# WARNING: Videos in /cache are retained until there are no ongoing events. If you are tracking cars or
|
||||
# other objects for long periods of time, the cache will continue to grow indefinitely.
|
||||
# NOTE: This feature does not work if you have "-vsync drop" configured in your input params.
|
||||
# This will only work for camera feeds that can be copied into the mp4 container format without
|
||||
# encoding such as h264. It may not work for some types of streams.
|
||||
################
|
||||
save_clips:
|
||||
enabled: False
|
||||
@@ -168,6 +201,11 @@ cameras:
|
||||
# Number of seconds before the event to include in the clips
|
||||
#########
|
||||
pre_capture: 30
|
||||
#########
|
||||
# Objects to save clips for. Defaults to all tracked object types.
|
||||
#########
|
||||
# objects:
|
||||
# - person
|
||||
|
||||
################
|
||||
# Configuration for the snapshots in the debug view and mqtt
|
||||
@@ -186,4 +224,5 @@ cameras:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.8
|
||||
min_score: 0.5
|
||||
threshold: 0.85
|
||||
|
||||
@@ -61,6 +61,7 @@ GLOBAL_OBJECT_CONFIG = CONFIG.get('objects', {})
|
||||
|
||||
WEB_PORT = CONFIG.get('web_port', 5000)
|
||||
DEBUG = (CONFIG.get('debug', '0') == '1')
|
||||
TENSORFLOW_DEVICE = CONFIG.get('tensorflow_device')
|
||||
|
||||
def start_plasma_store():
|
||||
plasma_cmd = ['plasma_store', '-m', '400000000', '-s', '/tmp/plasma']
|
||||
@@ -117,10 +118,10 @@ class CameraWatchdog(threading.Thread):
|
||||
camera_process['process_fps'].value = 0.0
|
||||
camera_process['detection_fps'].value = 0.0
|
||||
camera_process['read_start'].value = 0.0
|
||||
process = mp.Process(target=track_camera, args=(name, self.config[name], GLOBAL_OBJECT_CONFIG, camera_process['frame_queue'],
|
||||
process = mp.Process(target=track_camera, args=(name, self.config[name], camera_process['frame_queue'],
|
||||
camera_process['frame_shape'], self.tflite_process.detection_queue, self.tracked_objects_queue,
|
||||
camera_process['process_fps'], camera_process['detection_fps'],
|
||||
camera_process['read_start'], camera_process['detection_frame']))
|
||||
camera_process['read_start'], camera_process['detection_frame'], self.stop_event))
|
||||
process.daemon = True
|
||||
camera_process['process'] = process
|
||||
process.start()
|
||||
@@ -135,7 +136,7 @@ class CameraWatchdog(threading.Thread):
|
||||
camera_capture.start()
|
||||
camera_process['ffmpeg_process'] = ffmpeg_process
|
||||
camera_process['capture_thread'] = camera_capture
|
||||
elif now - camera_process['capture_thread'].current_frame > 5:
|
||||
elif now - camera_process['capture_thread'].current_frame.value > 5:
|
||||
print(f"No frames received from {name} in 5 seconds. Exiting ffmpeg...")
|
||||
ffmpeg_process = camera_process['ffmpeg_process']
|
||||
ffmpeg_process.terminate()
|
||||
@@ -181,6 +182,7 @@ def main():
|
||||
'show_timestamp': config.get('snapshots', {}).get('show_timestamp', True),
|
||||
'draw_zones': config.get('snapshots', {}).get('draw_zones', False)
|
||||
}
|
||||
config['zones'] = config.get('zones', {})
|
||||
|
||||
# Queue for cameras to push tracked objects to
|
||||
tracked_objects_queue = mp.Queue()
|
||||
@@ -189,7 +191,7 @@ def main():
|
||||
event_queue = mp.Queue()
|
||||
|
||||
# Start the shared tflite process
|
||||
tflite_process = EdgeTPUProcess()
|
||||
tflite_process = EdgeTPUProcess(TENSORFLOW_DEVICE)
|
||||
|
||||
# start the camera processes
|
||||
camera_processes = {}
|
||||
@@ -201,6 +203,8 @@ def main():
|
||||
ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', FFMPEG_DEFAULT_CONFIG['hwaccel_args'])
|
||||
ffmpeg_input_args = ffmpeg.get('input_args', FFMPEG_DEFAULT_CONFIG['input_args'])
|
||||
ffmpeg_output_args = ffmpeg.get('output_args', FFMPEG_DEFAULT_CONFIG['output_args'])
|
||||
if not config.get('fps') is None:
|
||||
ffmpeg_output_args = ["-r", str(config.get('fps'))] + ffmpeg_output_args
|
||||
if config.get('save_clips', {}).get('enabled', False):
|
||||
ffmpeg_output_args = [
|
||||
"-f",
|
||||
@@ -259,10 +263,26 @@ def main():
|
||||
'capture_thread': camera_capture
|
||||
}
|
||||
|
||||
camera_process = mp.Process(target=track_camera, args=(name, config, GLOBAL_OBJECT_CONFIG, frame_queue, frame_shape,
|
||||
# merge global object config into camera object config
|
||||
camera_objects_config = config.get('objects', {})
|
||||
# get objects to track for camera
|
||||
objects_to_track = camera_objects_config.get('track', GLOBAL_OBJECT_CONFIG.get('track', ['person']))
|
||||
# merge object filters
|
||||
global_object_filters = GLOBAL_OBJECT_CONFIG.get('filters', {})
|
||||
camera_object_filters = camera_objects_config.get('filters', {})
|
||||
objects_with_config = set().union(global_object_filters.keys(), camera_object_filters.keys())
|
||||
object_filters = {}
|
||||
for obj in objects_with_config:
|
||||
object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
|
||||
config['objects'] = {
|
||||
'track': objects_to_track,
|
||||
'filters': object_filters
|
||||
}
|
||||
|
||||
camera_process = mp.Process(target=track_camera, args=(name, config, frame_queue, frame_shape,
|
||||
tflite_process.detection_queue, tracked_objects_queue, camera_processes[name]['process_fps'],
|
||||
camera_processes[name]['detection_fps'],
|
||||
camera_processes[name]['read_start'], camera_processes[name]['detection_frame']))
|
||||
camera_processes[name]['read_start'], camera_processes[name]['detection_frame'], stop_event))
|
||||
camera_process.daemon = True
|
||||
camera_processes[name]['process'] = camera_process
|
||||
|
||||
@@ -270,10 +290,10 @@ def main():
|
||||
camera_process['process'].start()
|
||||
print(f"Camera_process started for {name}: {camera_process['process'].pid}")
|
||||
|
||||
event_processor = EventProcessor(CONFIG['cameras'], camera_processes, '/cache', '/clips', event_queue, stop_event)
|
||||
event_processor = EventProcessor(CONFIG, camera_processes, '/cache', '/clips', event_queue, stop_event)
|
||||
event_processor.start()
|
||||
|
||||
object_processor = TrackedObjectProcessor(CONFIG['cameras'], CONFIG.get('zones', {}), client, MQTT_TOPIC_PREFIX, tracked_objects_queue, event_queue,stop_event)
|
||||
object_processor = TrackedObjectProcessor(CONFIG['cameras'], client, MQTT_TOPIC_PREFIX, tracked_objects_queue, event_queue, stop_event)
|
||||
object_processor.start()
|
||||
|
||||
camera_watchdog = CameraWatchdog(camera_processes, CONFIG['cameras'], tflite_process, tracked_objects_queue, plasma_process, stop_event)
|
||||
@@ -340,7 +360,7 @@ def main():
|
||||
'pid': camera_stats['process'].pid,
|
||||
'ffmpeg_pid': camera_stats['ffmpeg_process'].pid,
|
||||
'frame_info': {
|
||||
'read': capture_thread.current_frame,
|
||||
'read': capture_thread.current_frame.value,
|
||||
'detect': camera_stats['detection_frame'].value,
|
||||
'process': object_processor.camera_data[name]['current_frame_time']
|
||||
}
|
||||
@@ -361,10 +381,14 @@ def main():
|
||||
@app.route('/<camera_name>/<label>/best.jpg')
|
||||
def best(camera_name, label):
|
||||
if camera_name in CONFIG['cameras']:
|
||||
best_frame = object_processor.get_best(camera_name, label)
|
||||
if best_frame is None:
|
||||
best_frame = np.zeros((720,1280,3), np.uint8)
|
||||
|
||||
best_object = object_processor.get_best(camera_name, label)
|
||||
best_frame = best_object.get('frame', np.zeros((720,1280,3), np.uint8))
|
||||
|
||||
crop = bool(request.args.get('crop', 0, type=int))
|
||||
if crop:
|
||||
region = best_object.get('region', [0,0,300,300])
|
||||
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
|
||||
|
||||
height = int(request.args.get('h', str(best_frame.shape[0])))
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
|
||||
|
||||
0
frigate/__init__.py
Normal file
0
frigate/__init__.py
Normal file
@@ -2,6 +2,7 @@ import os
|
||||
import datetime
|
||||
import hashlib
|
||||
import multiprocessing as mp
|
||||
from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
import pyarrow.plasma as plasma
|
||||
import tflite_runtime.interpreter as tflite
|
||||
@@ -27,13 +28,35 @@ def load_labels(path, encoding='utf-8'):
|
||||
else:
|
||||
return {index: line.strip() for index, line in enumerate(lines)}
|
||||
|
||||
class ObjectDetector():
|
||||
def __init__(self):
|
||||
class ObjectDetector(ABC):
|
||||
@abstractmethod
|
||||
def detect(self, tensor_input, threshold = .4):
|
||||
pass
|
||||
|
||||
class LocalObjectDetector(ObjectDetector):
|
||||
def __init__(self, tf_device=None, labels=None):
|
||||
self.fps = EventsPerSecond()
|
||||
if labels is None:
|
||||
self.labels = {}
|
||||
else:
|
||||
self.labels = load_labels(labels)
|
||||
|
||||
device_config = {"device": "usb"}
|
||||
if not tf_device is None:
|
||||
device_config = {"device": tf_device}
|
||||
|
||||
edge_tpu_delegate = None
|
||||
try:
|
||||
edge_tpu_delegate = load_delegate('libedgetpu.so.1.0')
|
||||
print(f"Attempting to load TPU as {device_config['device']}")
|
||||
edge_tpu_delegate = load_delegate('libedgetpu.so.1.0', device_config)
|
||||
print("TPU found")
|
||||
except ValueError:
|
||||
print("No EdgeTPU detected. Falling back to CPU.")
|
||||
try:
|
||||
print(f"Attempting to load TPU as pci:0")
|
||||
edge_tpu_delegate = load_delegate('libedgetpu.so.1.0', {"device": "pci:0"})
|
||||
print("PCIe TPU found")
|
||||
except ValueError:
|
||||
print("No EdgeTPU detected. Falling back to CPU.")
|
||||
|
||||
if edge_tpu_delegate is None:
|
||||
self.interpreter = tflite.Interpreter(
|
||||
@@ -48,6 +71,22 @@ class ObjectDetector():
|
||||
self.tensor_input_details = self.interpreter.get_input_details()
|
||||
self.tensor_output_details = self.interpreter.get_output_details()
|
||||
|
||||
def detect(self, tensor_input, threshold=.4):
|
||||
detections = []
|
||||
|
||||
raw_detections = self.detect_raw(tensor_input)
|
||||
|
||||
for d in raw_detections:
|
||||
if d[1] < threshold:
|
||||
break
|
||||
detections.append((
|
||||
self.labels[int(d[0])],
|
||||
float(d[1]),
|
||||
(d[2], d[3], d[4], d[5])
|
||||
))
|
||||
self.fps.update()
|
||||
return detections
|
||||
|
||||
def detect_raw(self, tensor_input):
|
||||
self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
|
||||
self.interpreter.invoke()
|
||||
@@ -61,11 +100,11 @@ class ObjectDetector():
|
||||
|
||||
return detections
|
||||
|
||||
def run_detector(detection_queue, avg_speed, start):
|
||||
def run_detector(detection_queue, avg_speed, start, tf_device):
|
||||
print(f"Starting detection process: {os.getpid()}")
|
||||
listen()
|
||||
plasma_client = plasma.connect("/tmp/plasma")
|
||||
object_detector = ObjectDetector()
|
||||
object_detector = LocalObjectDetector(tf_device=tf_device)
|
||||
|
||||
while True:
|
||||
object_id_str = detection_queue.get()
|
||||
@@ -86,11 +125,12 @@ def run_detector(detection_queue, avg_speed, start):
|
||||
avg_speed.value = (avg_speed.value*9 + duration)/10
|
||||
|
||||
class EdgeTPUProcess():
|
||||
def __init__(self):
|
||||
def __init__(self, tf_device=None):
|
||||
self.detection_queue = mp.Queue()
|
||||
self.avg_inference_speed = mp.Value('d', 0.01)
|
||||
self.detection_start = mp.Value('d', 0.0)
|
||||
self.detect_process = None
|
||||
self.tf_device = tf_device
|
||||
self.start_or_restart()
|
||||
|
||||
def start_or_restart(self):
|
||||
@@ -103,7 +143,7 @@ class EdgeTPUProcess():
|
||||
print("Detection process didnt exit. Force killing...")
|
||||
self.detect_process.kill()
|
||||
self.detect_process.join()
|
||||
self.detect_process = mp.Process(target=run_detector, args=(self.detection_queue, self.avg_inference_speed, self.detection_start))
|
||||
self.detect_process = mp.Process(target=run_detector, args=(self.detection_queue, self.avg_inference_speed, self.detection_start, self.tf_device))
|
||||
self.detect_process.daemon = True
|
||||
self.detect_process.start()
|
||||
|
||||
@@ -139,4 +179,4 @@ class RemoteObjectDetector():
|
||||
))
|
||||
self.plasma_client.delete([object_id_frame, object_id_detections])
|
||||
self.fps.update()
|
||||
return detections
|
||||
return detections
|
||||
|
||||
@@ -73,6 +73,11 @@ class EventProcessor(threading.Thread):
|
||||
earliest_event = min(self.events_in_process.values(), key=lambda x:x['start_time'])['start_time']
|
||||
else:
|
||||
earliest_event = datetime.datetime.now().timestamp()
|
||||
|
||||
# if the earliest event exceeds the max seconds, cap it
|
||||
max_seconds = self.config.get('save_clips', {}).get('max_seconds', 300)
|
||||
if datetime.datetime.now().timestamp()-earliest_event > max_seconds:
|
||||
earliest_event = datetime.datetime.now().timestamp()-max_seconds
|
||||
|
||||
for f, data in list(self.cached_clips.items()):
|
||||
if earliest_event-90 > data['start_time']+data['duration']:
|
||||
@@ -147,12 +152,23 @@ class EventProcessor(threading.Thread):
|
||||
|
||||
self.refresh_cache()
|
||||
|
||||
save_clips_config = self.config['cameras'][camera].get('save_clips', {})
|
||||
|
||||
# if save clips is not enabled for this camera, just continue
|
||||
if not save_clips_config.get('enabled', False):
|
||||
continue
|
||||
|
||||
# if specific objects are listed for this camera, only save clips for them
|
||||
if 'objects' in save_clips_config:
|
||||
if not event_data['label'] in save_clips_config['objects']:
|
||||
continue
|
||||
|
||||
if event_type == 'start':
|
||||
self.events_in_process[event_data['id']] = event_data
|
||||
|
||||
if event_type == 'end':
|
||||
if self.config[camera].get('save_clips', {}).get('enabled', False) and len(self.cached_clips) > 0:
|
||||
self.create_clip(camera, event_data, self.config[camera].get('save_clips', {}).get('pre_capture', 30))
|
||||
if len(self.cached_clips) > 0 and not event_data['false_positive']:
|
||||
self.create_clip(camera, event_data, save_clips_config.get('pre_capture', 30))
|
||||
del self.events_in_process[event_data['id']]
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@ import copy
|
||||
import cv2
|
||||
import threading
|
||||
import queue
|
||||
import copy
|
||||
import numpy as np
|
||||
from collections import Counter, defaultdict
|
||||
import itertools
|
||||
import pyarrow.plasma as plasma
|
||||
import matplotlib.pyplot as plt
|
||||
from frigate.util import draw_box_with_label, PlasmaManager
|
||||
from frigate.util import draw_box_with_label, PlasmaFrameManager
|
||||
from frigate.edgetpu import load_labels
|
||||
from typing import Callable, Dict
|
||||
from statistics import mean, median
|
||||
|
||||
PATH_TO_LABELS = '/labelmap.txt'
|
||||
|
||||
@@ -23,11 +26,6 @@ COLOR_MAP = {}
|
||||
for key, val in LABELS.items():
|
||||
COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
|
||||
|
||||
def filter_false_positives(event):
|
||||
if len(event['history']) < 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
def zone_filtered(obj, object_config):
|
||||
object_name = obj['label']
|
||||
object_filters = object_config.get('filters', {})
|
||||
@@ -46,21 +44,244 @@ def zone_filtered(obj, object_config):
|
||||
return True
|
||||
|
||||
# if the score is lower than the threshold, skip
|
||||
if obj_settings.get('threshold', 0) > obj['score']:
|
||||
if obj_settings.get('threshold', 0) > obj['computed_score']:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Maintains the state of a camera
|
||||
class CameraState():
|
||||
def __init__(self, name, config, frame_manager):
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.frame_manager = frame_manager
|
||||
|
||||
self.best_objects = {}
|
||||
self.object_status = defaultdict(lambda: 'OFF')
|
||||
self.tracked_objects = {}
|
||||
self.zone_objects = defaultdict(lambda: [])
|
||||
self.current_frame = np.zeros((720,1280,3), np.uint8)
|
||||
self.current_frame_time = 0.0
|
||||
self.previous_frame_id = None
|
||||
self.callbacks = defaultdict(lambda: [])
|
||||
|
||||
def false_positive(self, obj):
|
||||
# once a true positive, always a true positive
|
||||
if not obj.get('false_positive', True):
|
||||
return False
|
||||
|
||||
threshold = self.config['objects'].get('filters', {}).get(obj['label'], {}).get('threshold', 0.85)
|
||||
if obj['computed_score'] < threshold:
|
||||
return True
|
||||
return False
|
||||
|
||||
def compute_score(self, obj):
|
||||
scores = obj['score_history'][:]
|
||||
# pad with zeros if you dont have at least 3 scores
|
||||
if len(scores) < 3:
|
||||
scores += [0.0]*(3 - len(scores))
|
||||
return median(scores)
|
||||
|
||||
def on(self, event_type: str, callback: Callable[[Dict], None]):
|
||||
self.callbacks[event_type].append(callback)
|
||||
|
||||
def update(self, frame_time, tracked_objects):
|
||||
self.current_frame_time = frame_time
|
||||
# get the new frame and delete the old frame
|
||||
frame_id = f"{self.name}{frame_time}"
|
||||
self.current_frame = self.frame_manager.get(frame_id)
|
||||
if not self.previous_frame_id is None:
|
||||
self.frame_manager.delete(self.previous_frame_id)
|
||||
self.previous_frame_id = frame_id
|
||||
|
||||
current_ids = tracked_objects.keys()
|
||||
previous_ids = self.tracked_objects.keys()
|
||||
removed_ids = list(set(previous_ids).difference(current_ids))
|
||||
new_ids = list(set(current_ids).difference(previous_ids))
|
||||
updated_ids = list(set(current_ids).intersection(previous_ids))
|
||||
|
||||
for id in new_ids:
|
||||
self.tracked_objects[id] = tracked_objects[id]
|
||||
self.tracked_objects[id]['zones'] = []
|
||||
|
||||
# start the score history
|
||||
self.tracked_objects[id]['score_history'] = [self.tracked_objects[id]['score']]
|
||||
|
||||
# calculate if this is a false positive
|
||||
self.tracked_objects[id]['computed_score'] = self.compute_score(self.tracked_objects[id])
|
||||
self.tracked_objects[id]['false_positive'] = self.false_positive(self.tracked_objects[id])
|
||||
|
||||
# call event handlers
|
||||
for c in self.callbacks['start']:
|
||||
c(self.name, tracked_objects[id])
|
||||
|
||||
for id in updated_ids:
|
||||
self.tracked_objects[id].update(tracked_objects[id])
|
||||
|
||||
# if the object is not in the current frame, add a 0.0 to the score history
|
||||
if self.tracked_objects[id]['frame_time'] != self.current_frame_time:
|
||||
self.tracked_objects[id]['score_history'].append(0.0)
|
||||
else:
|
||||
self.tracked_objects[id]['score_history'].append(self.tracked_objects[id]['score'])
|
||||
# only keep the last 10 scores
|
||||
if len(self.tracked_objects[id]['score_history']) > 10:
|
||||
self.tracked_objects[id]['score_history'] = self.tracked_objects[id]['score_history'][-10:]
|
||||
|
||||
# calculate if this is a false positive
|
||||
self.tracked_objects[id]['computed_score'] = self.compute_score(self.tracked_objects[id])
|
||||
self.tracked_objects[id]['false_positive'] = self.false_positive(self.tracked_objects[id])
|
||||
|
||||
# call event handlers
|
||||
for c in self.callbacks['update']:
|
||||
c(self.name, self.tracked_objects[id])
|
||||
|
||||
for id in removed_ids:
|
||||
# publish events to mqtt
|
||||
self.tracked_objects[id]['end_time'] = frame_time
|
||||
for c in self.callbacks['end']:
|
||||
c(self.name, self.tracked_objects[id])
|
||||
del self.tracked_objects[id]
|
||||
|
||||
# check to see if the objects are in any zones
|
||||
for obj in self.tracked_objects.values():
|
||||
current_zones = []
|
||||
bottom_center = (obj['centroid'][0], obj['box'][3])
|
||||
# check each zone
|
||||
for name, zone in self.config['zones'].items():
|
||||
contour = zone['contour']
|
||||
# check if the object is in the zone and not filtered
|
||||
if (cv2.pointPolygonTest(contour, bottom_center, False) >= 0
|
||||
and not zone_filtered(obj, zone.get('filters', {}))):
|
||||
current_zones.append(name)
|
||||
obj['zones'] = current_zones
|
||||
|
||||
# draw on the frame
|
||||
if not self.current_frame is None:
|
||||
# draw the bounding boxes on the frame
|
||||
for obj in self.tracked_objects.values():
|
||||
thickness = 2
|
||||
color = COLOR_MAP[obj['label']]
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj['box']
|
||||
draw_box_with_label(self.current_frame, box[0], box[1], box[2], box[3], obj['label'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color)
|
||||
# draw the regions on the frame
|
||||
region = obj['region']
|
||||
cv2.rectangle(self.current_frame, (region[0], region[1]), (region[2], region[3]), (0,255,0), 1)
|
||||
|
||||
if self.config['snapshots']['show_timestamp']:
|
||||
time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S")
|
||||
cv2.putText(self.current_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
|
||||
|
||||
if self.config['snapshots']['draw_zones']:
|
||||
for name, zone in self.config['zones'].items():
|
||||
thickness = 8 if any([name in obj['zones'] for obj in self.tracked_objects.values()]) else 2
|
||||
cv2.drawContours(self.current_frame, [zone['contour']], -1, zone['color'], thickness)
|
||||
|
||||
# maintain best objects
|
||||
for obj in self.tracked_objects.values():
|
||||
object_type = obj['label']
|
||||
# if the object wasn't seen on the current frame, skip it
|
||||
if obj['frame_time'] != self.current_frame_time or obj['false_positive']:
|
||||
continue
|
||||
obj_copy = copy.deepcopy(obj)
|
||||
if object_type in self.best_objects:
|
||||
current_best = self.best_objects[object_type]
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# if the object is a higher score than the current best score
|
||||
# or the current object is older than desired, use the new object
|
||||
if obj_copy['score'] > current_best['score'] or (now - current_best['frame_time']) > self.config.get('best_image_timeout', 60):
|
||||
obj_copy['frame'] = np.copy(self.current_frame)
|
||||
self.best_objects[object_type] = obj_copy
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[object_type])
|
||||
else:
|
||||
obj_copy['frame'] = np.copy(self.current_frame)
|
||||
self.best_objects[object_type] = obj_copy
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[object_type])
|
||||
|
||||
# update overall camera state for each object type
|
||||
obj_counter = Counter()
|
||||
for obj in self.tracked_objects.values():
|
||||
if not obj['false_positive']:
|
||||
obj_counter[obj['label']] += 1
|
||||
|
||||
# report on detected objects
|
||||
for obj_name, count in obj_counter.items():
|
||||
new_status = 'ON' if count > 0 else 'OFF'
|
||||
if new_status != self.object_status[obj_name]:
|
||||
self.object_status[obj_name] = new_status
|
||||
for c in self.callbacks['object_status']:
|
||||
c(self.name, obj_name, new_status)
|
||||
|
||||
# expire any objects that are ON and no longer detected
|
||||
expired_objects = [obj_name for obj_name, status in self.object_status.items() if status == 'ON' and not obj_name in obj_counter]
|
||||
for obj_name in expired_objects:
|
||||
self.object_status[obj_name] = 'OFF'
|
||||
for c in self.callbacks['object_status']:
|
||||
c(self.name, obj_name, 'OFF')
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[obj_name])
|
||||
|
||||
|
||||
class TrackedObjectProcessor(threading.Thread):
|
||||
def __init__(self, camera_config, zone_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):
|
||||
threading.Thread.__init__(self)
|
||||
self.camera_config = camera_config
|
||||
self.zone_config = zone_config
|
||||
self.client = client
|
||||
self.topic_prefix = topic_prefix
|
||||
self.tracked_objects_queue = tracked_objects_queue
|
||||
self.event_queue = event_queue
|
||||
self.stop_event = stop_event
|
||||
self.camera_states: Dict[str, CameraState] = {}
|
||||
self.plasma_client = PlasmaFrameManager(self.stop_event)
|
||||
|
||||
def start(camera, obj):
|
||||
# publish events to mqtt
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/events/start", json.dumps(obj), retain=False)
|
||||
self.event_queue.put(('start', camera, obj))
|
||||
|
||||
def update(camera, obj):
|
||||
pass
|
||||
|
||||
def end(camera, obj):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/events/end", json.dumps(obj), retain=False)
|
||||
self.event_queue.put(('end', camera, obj))
|
||||
|
||||
def snapshot(camera, obj):
|
||||
if not 'frame' in obj:
|
||||
return
|
||||
best_frame = cv2.cvtColor(obj['frame'], cv2.COLOR_RGB2BGR)
|
||||
mqtt_config = self.camera_config[camera].get('mqtt', {'crop_to_region': False})
|
||||
if mqtt_config.get('crop_to_region'):
|
||||
region = obj['region']
|
||||
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
|
||||
if 'snapshot_height' in mqtt_config:
|
||||
height = int(mqtt_config['snapshot_height'])
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
jpg_bytes = jpg.tobytes()
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj['label']}/snapshot", jpg_bytes, retain=True)
|
||||
|
||||
def object_status(camera, object_name, status):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
|
||||
|
||||
for camera in self.camera_config.keys():
|
||||
camera_state = CameraState(camera, self.camera_config[camera], self.plasma_client)
|
||||
camera_state.on('start', start)
|
||||
camera_state.on('update', update)
|
||||
camera_state.on('end', end)
|
||||
camera_state.on('snapshot', snapshot)
|
||||
camera_state.on('object_status', object_status)
|
||||
self.camera_states[camera] = camera_state
|
||||
|
||||
self.camera_data = defaultdict(lambda: {
|
||||
'best_objects': {},
|
||||
'object_status': defaultdict(lambda: defaultdict(lambda: 'OFF')),
|
||||
@@ -69,38 +290,42 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
'current_frame_time': 0.0,
|
||||
'object_id': None
|
||||
})
|
||||
self.zone_data = defaultdict(lambda: {
|
||||
'object_status': defaultdict(lambda: defaultdict(lambda: 'OFF')),
|
||||
'contours': {}
|
||||
})
|
||||
# {
|
||||
# 'zone_name': {
|
||||
# 'person': ['camera_1', 'camera_2']
|
||||
# }
|
||||
# }
|
||||
self.zone_data = defaultdict(lambda: defaultdict(lambda: set()))
|
||||
|
||||
# set colors for zones
|
||||
all_zone_names = set([zone for config in self.camera_config.values() for zone in config['zones'].keys()])
|
||||
zone_colors = {}
|
||||
colors = plt.cm.get_cmap('tab10', len(all_zone_names))
|
||||
for i, zone in enumerate(all_zone_names):
|
||||
zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
|
||||
|
||||
# create zone contours
|
||||
for name, config in zone_config.items():
|
||||
for camera, camera_zone_config in config.items():
|
||||
coordinates = camera_zone_config['coordinates']
|
||||
for camera_config in self.camera_config.values():
|
||||
for zone_name, zone_config in camera_config['zones'].items():
|
||||
zone_config['color'] = zone_colors[zone_name]
|
||||
coordinates = zone_config['coordinates']
|
||||
if isinstance(coordinates, list):
|
||||
self.zone_data[name]['contours'][camera] = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in coordinates])
|
||||
zone_config['contour'] = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in coordinates])
|
||||
elif isinstance(coordinates, str):
|
||||
points = coordinates.split(',')
|
||||
self.zone_data[name]['contours'][camera] = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
|
||||
zone_config['contour'] = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
|
||||
else:
|
||||
print(f"Unable to parse zone coordinates for {name} - {camera}")
|
||||
|
||||
# set colors for zones
|
||||
colors = plt.cm.get_cmap('tab10', len(self.zone_data.keys()))
|
||||
for i, zone in enumerate(self.zone_data.values()):
|
||||
zone['color'] = tuple(int(round(255 * c)) for c in colors(i)[:3])
|
||||
|
||||
self.plasma_client = PlasmaManager(self.stop_event)
|
||||
print(f"Unable to parse zone coordinates for {zone_name} - {camera}")
|
||||
|
||||
def get_best(self, camera, label):
|
||||
if label in self.camera_data[camera]['best_objects']:
|
||||
return self.camera_data[camera]['best_objects'][label]['frame']
|
||||
best_objects = self.camera_states[camera].best_objects
|
||||
if label in best_objects:
|
||||
return best_objects[label]
|
||||
else:
|
||||
return None
|
||||
return {}
|
||||
|
||||
def get_current_frame(self, camera):
|
||||
return self.camera_data[camera]['current_frame']
|
||||
return self.camera_states[camera].current_frame
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
@@ -113,165 +338,27 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
camera_config = self.camera_config[camera]
|
||||
best_objects = self.camera_data[camera]['best_objects']
|
||||
current_object_status = self.camera_data[camera]['object_status']
|
||||
tracked_objects = self.camera_data[camera]['tracked_objects']
|
||||
camera_state = self.camera_states[camera]
|
||||
|
||||
current_ids = current_tracked_objects.keys()
|
||||
previous_ids = tracked_objects.keys()
|
||||
removed_ids = list(set(previous_ids).difference(current_ids))
|
||||
new_ids = list(set(current_ids).difference(previous_ids))
|
||||
updated_ids = list(set(current_ids).intersection(previous_ids))
|
||||
camera_state.update(frame_time, current_tracked_objects)
|
||||
|
||||
for id in new_ids:
|
||||
# only register the object here if we are sure it isnt a false positive
|
||||
if not filter_false_positives(current_tracked_objects[id]):
|
||||
tracked_objects[id] = current_tracked_objects[id]
|
||||
# publish events to mqtt
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/events/start", json.dumps(tracked_objects[id]), retain=False)
|
||||
self.event_queue.put(('start', camera, tracked_objects[id]))
|
||||
|
||||
for id in updated_ids:
|
||||
tracked_objects[id] = current_tracked_objects[id]
|
||||
|
||||
for id in removed_ids:
|
||||
# publish events to mqtt
|
||||
tracked_objects[id]['end_time'] = frame_time
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/events/end", json.dumps(tracked_objects[id]), retain=False)
|
||||
self.event_queue.put(('end', camera, tracked_objects[id]))
|
||||
del tracked_objects[id]
|
||||
|
||||
self.camera_data[camera]['current_frame_time'] = frame_time
|
||||
|
||||
# build a dict of objects in each zone for current camera
|
||||
current_objects_in_zones = defaultdict(lambda: [])
|
||||
for obj in tracked_objects.values():
|
||||
bottom_center = (obj['centroid'][0], obj['box'][3])
|
||||
# check each zone
|
||||
for name, zone in self.zone_data.items():
|
||||
current_contour = zone['contours'].get(camera, None)
|
||||
# if the current camera does not have a contour for this zone, skip
|
||||
if current_contour is None:
|
||||
continue
|
||||
# check if the object is in the zone and not filtered
|
||||
if (cv2.pointPolygonTest(current_contour, bottom_center, False) >= 0
|
||||
and not zone_filtered(obj, self.zone_config[name][camera].get('filters', {}))):
|
||||
current_objects_in_zones[name].append(obj['label'])
|
||||
|
||||
###
|
||||
# Draw tracked objects on the frame
|
||||
###
|
||||
current_frame = self.plasma_client.get(f"{camera}{frame_time}")
|
||||
|
||||
if not current_frame is plasma.ObjectNotAvailable:
|
||||
# draw the bounding boxes on the frame
|
||||
for obj in tracked_objects.values():
|
||||
thickness = 2
|
||||
color = COLOR_MAP[obj['label']]
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj['box']
|
||||
draw_box_with_label(current_frame, box[0], box[1], box[2], box[3], obj['label'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color)
|
||||
# draw the regions on the frame
|
||||
region = obj['region']
|
||||
cv2.rectangle(current_frame, (region[0], region[1]), (region[2], region[3]), (0,255,0), 1)
|
||||
|
||||
if camera_config['snapshots']['show_timestamp']:
|
||||
time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S")
|
||||
cv2.putText(current_frame, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
|
||||
|
||||
if camera_config['snapshots']['draw_zones']:
|
||||
for name, zone in self.zone_data.items():
|
||||
thickness = 2 if len(current_objects_in_zones[name]) == 0 else 8
|
||||
if camera in zone['contours']:
|
||||
cv2.drawContours(current_frame, [zone['contours'][camera]], -1, zone['color'], thickness)
|
||||
|
||||
###
|
||||
# Set the current frame
|
||||
###
|
||||
self.camera_data[camera]['current_frame'] = current_frame
|
||||
|
||||
# delete the previous frame from the plasma store and update the object id
|
||||
if not self.camera_data[camera]['object_id'] is None:
|
||||
self.plasma_client.delete(self.camera_data[camera]['object_id'])
|
||||
self.camera_data[camera]['object_id'] = f"{camera}{frame_time}"
|
||||
|
||||
###
|
||||
# Maintain the highest scoring recent object and frame for each label
|
||||
###
|
||||
for obj in tracked_objects.values():
|
||||
# if the object wasn't seen on the current frame, skip it
|
||||
if obj['frame_time'] != frame_time:
|
||||
continue
|
||||
if obj['label'] in best_objects:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# if the object is a higher score than the current best score
|
||||
# or the current object is more than 1 minute old, use the new object
|
||||
if obj['score'] > best_objects[obj['label']]['score'] or (now - best_objects[obj['label']]['frame_time']) > 60:
|
||||
obj['frame'] = np.copy(self.camera_data[camera]['current_frame'])
|
||||
best_objects[obj['label']] = obj
|
||||
# send updated snapshot over mqtt
|
||||
best_frame = cv2.cvtColor(obj['frame'], cv2.COLOR_RGB2BGR)
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
jpg_bytes = jpg.tobytes()
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj['label']}/snapshot", jpg_bytes, retain=True)
|
||||
else:
|
||||
obj['frame'] = np.copy(self.camera_data[camera]['current_frame'])
|
||||
best_objects[obj['label']] = obj
|
||||
|
||||
###
|
||||
# Report over MQTT
|
||||
###
|
||||
|
||||
# get the zones that are relevant for this camera
|
||||
relevant_zones = [zone for zone, config in self.zone_config.items() if camera in config]
|
||||
for zone in relevant_zones:
|
||||
# create the set of labels in the current frame and previously reported
|
||||
labels_for_zone = set(current_objects_in_zones[zone] + list(self.zone_data[zone]['object_status'][camera].keys()))
|
||||
# for each label
|
||||
for label in labels_for_zone:
|
||||
# compute the current 'ON' vs 'OFF' status by checking if any camera sees the object in the zone
|
||||
previous_state = any([c[label] == 'ON' for c in self.zone_data[zone]['object_status'].values()])
|
||||
self.zone_data[zone]['object_status'][camera][label] = 'ON' if label in current_objects_in_zones[zone] else 'OFF'
|
||||
new_state = any([c[label] == 'ON' for c in self.zone_data[zone]['object_status'].values()])
|
||||
# update zone status for each label
|
||||
for zone in camera_state.config['zones'].keys():
|
||||
# get labels for current camera and all labels in current zone
|
||||
labels_for_camera = set([obj['label'] for obj in camera_state.tracked_objects.values() if zone in obj['zones'] and not obj['false_positive']])
|
||||
labels_to_check = labels_for_camera | set(self.zone_data[zone].keys())
|
||||
# for each label in zone
|
||||
for label in labels_to_check:
|
||||
camera_list = self.zone_data[zone][label]
|
||||
# remove or add the camera to the list for the current label
|
||||
previous_state = len(camera_list) > 0
|
||||
if label in labels_for_camera:
|
||||
camera_list.add(camera_state.name)
|
||||
elif camera_state.name in camera_list:
|
||||
camera_list.remove(camera_state.name)
|
||||
new_state = len(camera_list) > 0
|
||||
# if the value is changing, send over MQTT
|
||||
if previous_state == False and new_state == True:
|
||||
self.client.publish(f"{self.topic_prefix}/{zone}/{label}", 'ON', retain=False)
|
||||
elif previous_state == True and new_state == False:
|
||||
self.client.publish(f"{self.topic_prefix}/{zone}/{label}", 'OFF', retain=False)
|
||||
|
||||
# count by type
|
||||
obj_counter = Counter()
|
||||
for obj in tracked_objects.values():
|
||||
obj_counter[obj['label']] += 1
|
||||
|
||||
# report on detected objects
|
||||
for obj_name, count in obj_counter.items():
|
||||
new_status = 'ON' if count > 0 else 'OFF'
|
||||
if new_status != current_object_status[obj_name]:
|
||||
current_object_status[obj_name] = new_status
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}", new_status, retain=False)
|
||||
# send the best snapshot over mqtt
|
||||
best_frame = cv2.cvtColor(best_objects[obj_name]['frame'], cv2.COLOR_RGB2BGR)
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
jpg_bytes = jpg.tobytes()
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}/snapshot", jpg_bytes, retain=True)
|
||||
|
||||
# expire any objects that are ON and no longer detected
|
||||
expired_objects = [obj_name for obj_name, status in current_object_status.items() if status == 'ON' and not obj_name in obj_counter]
|
||||
for obj_name in expired_objects:
|
||||
current_object_status[obj_name] = 'OFF'
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}", 'OFF', retain=False)
|
||||
# send updated snapshot over mqtt
|
||||
best_frame = cv2.cvtColor(best_objects[obj_name]['frame'], cv2.COLOR_RGB2BGR)
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
jpg_bytes = jpg.tobytes()
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj_name}/snapshot", jpg_bytes, retain=True)
|
||||
|
||||
@@ -24,7 +24,6 @@ class ObjectTracker():
|
||||
obj['id'] = id
|
||||
obj['start_time'] = obj['frame_time']
|
||||
obj['top_score'] = obj['score']
|
||||
self.add_history(obj)
|
||||
self.tracked_objects[id] = obj
|
||||
self.disappeared[id] = 0
|
||||
|
||||
@@ -35,25 +34,8 @@ class ObjectTracker():
|
||||
def update(self, id, new_obj):
|
||||
self.disappeared[id] = 0
|
||||
self.tracked_objects[id].update(new_obj)
|
||||
self.add_history(self.tracked_objects[id])
|
||||
if self.tracked_objects[id]['score'] > self.tracked_objects[id]['top_score']:
|
||||
self.tracked_objects[id]['top_score'] = self.tracked_objects[id]['score']
|
||||
|
||||
def add_history(self, obj):
|
||||
entry = {
|
||||
'score': obj['score'],
|
||||
'box': obj['box'],
|
||||
'region': obj['region'],
|
||||
'centroid': obj['centroid'],
|
||||
'frame_time': obj['frame_time']
|
||||
}
|
||||
if 'history' in obj:
|
||||
obj['history'].append(entry)
|
||||
# only maintain the last 20 in history
|
||||
if len(obj['history']) > 20:
|
||||
obj['history'] = obj['history'][-20:]
|
||||
else:
|
||||
obj['history'] = [entry]
|
||||
|
||||
def match_and_update(self, frame_time, new_objects):
|
||||
# group by name
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import datetime
|
||||
import time
|
||||
import signal
|
||||
@@ -43,6 +44,9 @@ def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thicknes
|
||||
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
|
||||
# size is larger than longest edge
|
||||
size = int(max(xmax-xmin, ymax-ymin)*multiplier)
|
||||
# dont go any smaller than 300
|
||||
if size < 300:
|
||||
size = 300
|
||||
# if the size is too big to fit in the frame
|
||||
if size > min(frame_shape[0], frame_shape[1]):
|
||||
size = min(frame_shape[0], frame_shape[1])
|
||||
@@ -122,12 +126,16 @@ class EventsPerSecond:
|
||||
self._start = datetime.datetime.now().timestamp()
|
||||
|
||||
def update(self):
|
||||
if self._start is None:
|
||||
self.start()
|
||||
self._timestamps.append(datetime.datetime.now().timestamp())
|
||||
# truncate the list when it goes 100 over the max_size
|
||||
if len(self._timestamps) > self._max_events+100:
|
||||
self._timestamps = self._timestamps[(1-self._max_events):]
|
||||
|
||||
def eps(self, last_n_seconds=10):
|
||||
if self._start is None:
|
||||
self.start()
|
||||
# compute the (approximate) events in the last n seconds
|
||||
now = datetime.datetime.now().timestamp()
|
||||
seconds = min(now-self._start, last_n_seconds)
|
||||
@@ -139,7 +147,33 @@ def print_stack(sig, frame):
|
||||
def listen():
|
||||
signal.signal(signal.SIGUSR1, print_stack)
|
||||
|
||||
class PlasmaManager:
|
||||
class FrameManager(ABC):
|
||||
@abstractmethod
|
||||
def get(self, name, timeout_ms=0):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def put(self, name, frame):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, name):
|
||||
pass
|
||||
|
||||
class DictFrameManager(FrameManager):
|
||||
def __init__(self):
|
||||
self.frames = {}
|
||||
|
||||
def get(self, name, timeout_ms=0):
|
||||
return self.frames.get(name)
|
||||
|
||||
def put(self, name, frame):
|
||||
self.frames[name] = frame
|
||||
|
||||
def delete(self, name):
|
||||
del self.frames[name]
|
||||
|
||||
class PlasmaFrameManager(FrameManager):
|
||||
def __init__(self, stop_event=None):
|
||||
self.stop_event = stop_event
|
||||
self.connect()
|
||||
@@ -161,18 +195,21 @@ class PlasmaManager:
|
||||
if self.stop_event != None and self.stop_event.is_set():
|
||||
return
|
||||
try:
|
||||
return self.plasma_client.get(object_id, timeout_ms=timeout_ms)
|
||||
frame = self.plasma_client.get(object_id, timeout_ms=timeout_ms)
|
||||
if frame is plasma.ObjectNotAvailable:
|
||||
return None
|
||||
return frame
|
||||
except:
|
||||
self.connect()
|
||||
time.sleep(1)
|
||||
|
||||
def put(self, name, obj):
|
||||
def put(self, name, frame):
|
||||
object_id = plasma.ObjectID(hashlib.sha1(str.encode(name)).digest())
|
||||
while True:
|
||||
if self.stop_event != None and self.stop_event.is_set():
|
||||
return
|
||||
try:
|
||||
self.plasma_client.put(obj, object_id)
|
||||
self.plasma_client.put(frame, object_id)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Failed to put in plasma: {e}")
|
||||
|
||||
326
frigate/video.py
326
frigate/video.py
@@ -13,8 +13,9 @@ import copy
|
||||
import itertools
|
||||
import json
|
||||
import base64
|
||||
from typing import Dict, List
|
||||
from collections import defaultdict
|
||||
from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, PlasmaManager
|
||||
from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, FrameManager, PlasmaFrameManager
|
||||
from frigate.objects import ObjectTracker
|
||||
from frigate.edgetpu import RemoteObjectDetector
|
||||
from frigate.motion import MotionDetector
|
||||
@@ -53,7 +54,7 @@ def get_ffmpeg_input(ffmpeg_input):
|
||||
frigate_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
|
||||
return ffmpeg_input.format(**frigate_vars)
|
||||
|
||||
def filtered(obj, objects_to_track, object_filters, mask):
|
||||
def filtered(obj, objects_to_track, object_filters, mask=None):
|
||||
object_name = obj[0]
|
||||
|
||||
if not object_name in objects_to_track:
|
||||
@@ -72,8 +73,8 @@ def filtered(obj, objects_to_track, object_filters, mask):
|
||||
if obj_settings.get('max_area', 24000000) < obj[3]:
|
||||
return True
|
||||
|
||||
# if the score is lower than the threshold, skip
|
||||
if obj_settings.get('threshold', 0) > obj[1]:
|
||||
# if the score is lower than the min_score, skip
|
||||
if obj_settings.get('min_score', 0) > obj[1]:
|
||||
return True
|
||||
|
||||
# compute the coordinates of the object and make sure
|
||||
@@ -82,10 +83,10 @@ def filtered(obj, objects_to_track, object_filters, mask):
|
||||
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
|
||||
|
||||
# if the object is in a masked location, don't add it to detected objects
|
||||
if mask[y_location][x_location] == [0]:
|
||||
if (not mask is None) and (mask[y_location][x_location] == 0):
|
||||
return True
|
||||
|
||||
return False
|
||||
return False
|
||||
|
||||
def create_tensor_input(frame, region):
|
||||
cropped_frame = frame[region[1]:region[3], region[0]:region[2]]
|
||||
@@ -115,6 +116,53 @@ def start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process=None):
|
||||
process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
|
||||
return process
|
||||
|
||||
def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,
|
||||
frame_queue, take_frame: int, fps:EventsPerSecond, skipped_fps: EventsPerSecond,
|
||||
stop_event: mp.Event, detection_frame: mp.Value, current_frame: mp.Value):
|
||||
|
||||
frame_num = 0
|
||||
last_frame = 0
|
||||
frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
|
||||
skipped_fps.start()
|
||||
while True:
|
||||
if stop_event.is_set():
|
||||
print(f"{camera_name}: stop event set. exiting capture thread...")
|
||||
break
|
||||
|
||||
frame_bytes = ffmpeg_process.stdout.read(frame_size)
|
||||
current_frame.value = datetime.datetime.now().timestamp()
|
||||
|
||||
if len(frame_bytes) < frame_size:
|
||||
print(f"{camera_name}: ffmpeg sent a broken frame. something is wrong.")
|
||||
|
||||
if ffmpeg_process.poll() != None:
|
||||
print(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
fps.update()
|
||||
|
||||
frame_num += 1
|
||||
if (frame_num % take_frame) != 0:
|
||||
skipped_fps.update()
|
||||
continue
|
||||
|
||||
# if the detection process is more than 1 second behind, skip this frame
|
||||
if detection_frame.value > 0.0 and (last_frame - detection_frame.value) > 1:
|
||||
skipped_fps.update()
|
||||
continue
|
||||
|
||||
# put the frame in the frame manager
|
||||
frame_manager.put(f"{camera_name}{current_frame.value}",
|
||||
np
|
||||
.frombuffer(frame_bytes, np.uint8)
|
||||
.reshape(frame_shape)
|
||||
)
|
||||
# add to the queue
|
||||
frame_queue.put(current_frame.value)
|
||||
last_frame = current_frame.value
|
||||
|
||||
class CameraCapture(threading.Thread):
|
||||
def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
@@ -125,73 +173,28 @@ class CameraCapture(threading.Thread):
|
||||
self.take_frame = take_frame
|
||||
self.fps = fps
|
||||
self.skipped_fps = EventsPerSecond()
|
||||
self.plasma_client = PlasmaManager(stop_event)
|
||||
self.plasma_client = PlasmaFrameManager(stop_event)
|
||||
self.ffmpeg_process = ffmpeg_process
|
||||
self.current_frame = 0
|
||||
self.current_frame = mp.Value('d', 0.0)
|
||||
self.last_frame = 0
|
||||
self.detection_frame = detection_frame
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
frame_num = 0
|
||||
self.skipped_fps.start()
|
||||
while True:
|
||||
if self.stop_event.is_set():
|
||||
print(f"{self.name}: stop event set. exiting capture thread...")
|
||||
break
|
||||
capture_frames(self.ffmpeg_process, self.name, self.frame_shape, self.plasma_client, self.frame_queue, self.take_frame,
|
||||
self.fps, self.skipped_fps, self.stop_event, self.detection_frame, self.current_frame)
|
||||
|
||||
if self.ffmpeg_process.poll() != None:
|
||||
print(f"{self.name}: ffmpeg process is not running. exiting capture thread...")
|
||||
break
|
||||
|
||||
frame_bytes = self.ffmpeg_process.stdout.read(self.frame_size)
|
||||
self.current_frame = datetime.datetime.now().timestamp()
|
||||
|
||||
if len(frame_bytes) == 0:
|
||||
print(f"{self.name}: ffmpeg didnt return a frame. something is wrong.")
|
||||
continue
|
||||
|
||||
self.fps.update()
|
||||
|
||||
frame_num += 1
|
||||
if (frame_num % self.take_frame) != 0:
|
||||
self.skipped_fps.update()
|
||||
continue
|
||||
|
||||
# if the detection process is more than 1 second behind, skip this frame
|
||||
if self.detection_frame.value > 0.0 and (self.last_frame - self.detection_frame.value) > 1:
|
||||
self.skipped_fps.update()
|
||||
continue
|
||||
|
||||
# put the frame in the plasma store
|
||||
self.plasma_client.put(f"{self.name}{self.current_frame}",
|
||||
np
|
||||
.frombuffer(frame_bytes, np.uint8)
|
||||
.reshape(self.frame_shape)
|
||||
)
|
||||
# add to the queue
|
||||
self.frame_queue.put(self.current_frame)
|
||||
self.last_frame = self.current_frame
|
||||
|
||||
def track_camera(name, config, global_objects_config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame):
|
||||
def track_camera(name, config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame, stop_event):
|
||||
print(f"Starting process for {name}: {os.getpid()}")
|
||||
listen()
|
||||
|
||||
detection_frame.value = 0.0
|
||||
|
||||
# Merge the tracked object config with the global config
|
||||
camera_objects_config = config.get('objects', {})
|
||||
# combine tracked objects lists
|
||||
objects_to_track = set().union(global_objects_config.get('track', ['person', 'car', 'truck']), camera_objects_config.get('track', []))
|
||||
# merge object filters
|
||||
global_object_filters = global_objects_config.get('filters', {})
|
||||
camera_object_filters = camera_objects_config.get('filters', {})
|
||||
objects_with_config = set().union(global_object_filters.keys(), camera_object_filters.keys())
|
||||
object_filters = {}
|
||||
for obj in objects_with_config:
|
||||
object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
|
||||
|
||||
frame = np.zeros(frame_shape, np.uint8)
|
||||
camera_objects_config = config.get('objects', {})
|
||||
objects_to_track = camera_objects_config.get('track', [])
|
||||
object_filters = camera_objects_config.get('filters', {})
|
||||
|
||||
# load in the mask for object detection
|
||||
if 'mask' in config:
|
||||
@@ -199,13 +202,19 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
||||
img = base64.b64decode(config['mask'][7:])
|
||||
npimg = np.fromstring(img, dtype=np.uint8)
|
||||
mask = cv2.imdecode(npimg, cv2.IMREAD_GRAYSCALE)
|
||||
elif config['mask'].startswith('poly,'):
|
||||
points = config['mask'].split(',')[1:]
|
||||
contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
|
||||
mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8)
|
||||
mask[:] = 255
|
||||
cv2.fillPoly(mask, pts=[contour], color=(0))
|
||||
else:
|
||||
mask = cv2.imread("/config/{}".format(config['mask']), cv2.IMREAD_GRAYSCALE)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
if mask is None:
|
||||
mask = np.zeros((frame_shape[0], frame_shape[1], 1), np.uint8)
|
||||
if mask is None or mask.size == 0:
|
||||
mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8)
|
||||
mask[:] = 255
|
||||
|
||||
motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
|
||||
@@ -213,109 +222,100 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
||||
|
||||
object_tracker = ObjectTracker(10)
|
||||
|
||||
plasma_client = PlasmaManager()
|
||||
avg_wait = 0.0
|
||||
plasma_client = PlasmaFrameManager()
|
||||
|
||||
process_frames(name, frame_queue, frame_shape, plasma_client, motion_detector, object_detector,
|
||||
object_tracker, detected_objects_queue, fps, detection_fps, detection_frame, objects_to_track, object_filters, mask, stop_event)
|
||||
|
||||
print(f"{name}: exiting subprocess")
|
||||
|
||||
def reduce_boxes(boxes):
|
||||
if len(boxes) == 0:
|
||||
return []
|
||||
reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
|
||||
return [tuple(b) for b in reduced_boxes]
|
||||
|
||||
def detect(object_detector, frame, region, objects_to_track, object_filters, mask):
|
||||
tensor_input = create_tensor_input(frame, region)
|
||||
|
||||
detections = []
|
||||
region_detections = object_detector.detect(tensor_input)
|
||||
for d in region_detections:
|
||||
box = d[2]
|
||||
size = region[2]-region[0]
|
||||
x_min = int((box[1] * size) + region[0])
|
||||
y_min = int((box[0] * size) + region[1])
|
||||
x_max = int((box[3] * size) + region[0])
|
||||
y_max = int((box[2] * size) + region[1])
|
||||
det = (d[0],
|
||||
d[1],
|
||||
(x_min, y_min, x_max, y_max),
|
||||
(x_max-x_min)*(y_max-y_min),
|
||||
region)
|
||||
# apply object filters
|
||||
if filtered(det, objects_to_track, object_filters, mask):
|
||||
continue
|
||||
detections.append(det)
|
||||
return detections
|
||||
|
||||
def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
|
||||
frame_manager: FrameManager, motion_detector: MotionDetector,
|
||||
object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
|
||||
detected_objects_queue: mp.Queue, fps: mp.Value, detection_fps: mp.Value, current_frame_time: mp.Value,
|
||||
objects_to_track: List[str], object_filters: Dict, mask, stop_event: mp.Event,
|
||||
exit_on_empty: bool = False):
|
||||
|
||||
fps_tracker = EventsPerSecond()
|
||||
fps_tracker.start()
|
||||
object_detector.fps.start()
|
||||
while True:
|
||||
read_start.value = datetime.datetime.now().timestamp()
|
||||
frame_time = frame_queue.get()
|
||||
duration = datetime.datetime.now().timestamp()-read_start.value
|
||||
read_start.value = 0.0
|
||||
avg_wait = (avg_wait*99+duration)/100
|
||||
detection_frame.value = frame_time
|
||||
|
||||
# Get frame from plasma store
|
||||
frame = plasma_client.get(f"{name}{frame_time}")
|
||||
|
||||
if frame is plasma.ObjectNotAvailable:
|
||||
while True:
|
||||
if stop_event.is_set() or (exit_on_empty and frame_queue.empty()):
|
||||
print(f"Exiting track_objects...")
|
||||
break
|
||||
|
||||
try:
|
||||
frame_time = frame_queue.get(True, 10)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
|
||||
current_frame_time.value = frame_time
|
||||
|
||||
frame = frame_manager.get(f"{camera_name}{frame_time}")
|
||||
|
||||
if frame is None:
|
||||
print(f"{camera_name}: frame {frame_time} is not in memory store.")
|
||||
continue
|
||||
|
||||
fps_tracker.update()
|
||||
fps.value = fps_tracker.eps()
|
||||
detection_fps.value = object_detector.fps.eps()
|
||||
|
||||
|
||||
# look for motion
|
||||
motion_boxes = motion_detector.detect(frame)
|
||||
|
||||
tracked_objects = object_tracker.tracked_objects.values()
|
||||
tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
|
||||
|
||||
# merge areas of motion that intersect with a known tracked object into a single area to look at
|
||||
areas_of_interest = []
|
||||
used_motion_boxes = []
|
||||
for obj in tracked_objects:
|
||||
x_min, y_min, x_max, y_max = obj['box']
|
||||
for m_index, motion_box in enumerate(motion_boxes):
|
||||
if intersection_over_union(motion_box, obj['box']) > .2:
|
||||
used_motion_boxes.append(m_index)
|
||||
x_min = min(obj['box'][0], motion_box[0])
|
||||
y_min = min(obj['box'][1], motion_box[1])
|
||||
x_max = max(obj['box'][2], motion_box[2])
|
||||
y_max = max(obj['box'][3], motion_box[3])
|
||||
areas_of_interest.append((x_min, y_min, x_max, y_max))
|
||||
unused_motion_boxes = set(range(0, len(motion_boxes))).difference(used_motion_boxes)
|
||||
|
||||
# compute motion regions
|
||||
motion_regions = [calculate_region(frame_shape, motion_boxes[i][0], motion_boxes[i][1], motion_boxes[i][2], motion_boxes[i][3], 1.2)
|
||||
for i in unused_motion_boxes]
|
||||
|
||||
# compute tracked object regions
|
||||
object_regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
|
||||
for a in areas_of_interest]
|
||||
|
||||
# merge regions with high IOU
|
||||
merged_regions = motion_regions+object_regions
|
||||
while True:
|
||||
max_iou = 0.0
|
||||
max_indices = None
|
||||
region_indices = range(len(merged_regions))
|
||||
for a, b in itertools.combinations(region_indices, 2):
|
||||
iou = intersection_over_union(merged_regions[a], merged_regions[b])
|
||||
if iou > max_iou:
|
||||
max_iou = iou
|
||||
max_indices = (a, b)
|
||||
if max_iou > 0.1:
|
||||
a = merged_regions[max_indices[0]]
|
||||
b = merged_regions[max_indices[1]]
|
||||
merged_regions.append(calculate_region(frame_shape,
|
||||
min(a[0], b[0]),
|
||||
min(a[1], b[1]),
|
||||
max(a[2], b[2]),
|
||||
max(a[3], b[3]),
|
||||
1
|
||||
))
|
||||
del merged_regions[max(max_indices[0], max_indices[1])]
|
||||
del merged_regions[min(max_indices[0], max_indices[1])]
|
||||
else:
|
||||
break
|
||||
# combine motion boxes with known locations of existing objects
|
||||
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
|
||||
|
||||
# compute regions
|
||||
regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
|
||||
for a in combined_boxes]
|
||||
|
||||
# combine overlapping regions
|
||||
combined_regions = reduce_boxes(regions)
|
||||
|
||||
# re-compute regions
|
||||
regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
|
||||
for a in combined_regions]
|
||||
|
||||
# resize regions and detect
|
||||
detections = []
|
||||
for region in merged_regions:
|
||||
|
||||
tensor_input = create_tensor_input(frame, region)
|
||||
|
||||
region_detections = object_detector.detect(tensor_input)
|
||||
|
||||
for d in region_detections:
|
||||
box = d[2]
|
||||
size = region[2]-region[0]
|
||||
x_min = int((box[1] * size) + region[0])
|
||||
y_min = int((box[0] * size) + region[1])
|
||||
x_max = int((box[3] * size) + region[0])
|
||||
y_max = int((box[2] * size) + region[1])
|
||||
det = (d[0],
|
||||
d[1],
|
||||
(x_min, y_min, x_max, y_max),
|
||||
(x_max-x_min)*(y_max-y_min),
|
||||
region)
|
||||
if filtered(det, objects_to_track, object_filters, mask):
|
||||
continue
|
||||
detections.append(det)
|
||||
|
||||
for region in regions:
|
||||
detections.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
|
||||
|
||||
#########
|
||||
# merge objects, check for clipped objects and look again up to N times
|
||||
# merge objects, check for clipped objects and look again up to 4 times
|
||||
#########
|
||||
refining = True
|
||||
refine_count = 0
|
||||
@@ -345,40 +345,22 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
||||
box[0], box[1],
|
||||
box[2], box[3])
|
||||
|
||||
tensor_input = create_tensor_input(frame, region)
|
||||
# run detection on new region
|
||||
refined_detections = object_detector.detect(tensor_input)
|
||||
for d in refined_detections:
|
||||
box = d[2]
|
||||
size = region[2]-region[0]
|
||||
x_min = int((box[1] * size) + region[0])
|
||||
y_min = int((box[0] * size) + region[1])
|
||||
x_max = int((box[3] * size) + region[0])
|
||||
y_max = int((box[2] * size) + region[1])
|
||||
det = (d[0],
|
||||
d[1],
|
||||
(x_min, y_min, x_max, y_max),
|
||||
(x_max-x_min)*(y_max-y_min),
|
||||
region)
|
||||
if filtered(det, objects_to_track, object_filters, mask):
|
||||
continue
|
||||
selected_objects.append(det)
|
||||
selected_objects.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
|
||||
|
||||
refining = True
|
||||
else:
|
||||
selected_objects.append(obj)
|
||||
|
||||
selected_objects.append(obj)
|
||||
# set the detections list to only include top, complete objects
|
||||
# and new detections
|
||||
detections = selected_objects
|
||||
|
||||
if refining:
|
||||
refine_count += 1
|
||||
|
||||
|
||||
# now that we have refined our detections, we need to track objects
|
||||
object_tracker.match_and_update(frame_time, detections)
|
||||
|
||||
# add to the queue
|
||||
detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects))
|
||||
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects))
|
||||
|
||||
print(f"{name}: exiting subprocess")
|
||||
detection_fps.value = object_detector.fps.eps()
|
||||
|
||||
80
labelmap.txt
Normal file
80
labelmap.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
0 person
|
||||
1 bicycle
|
||||
2 car
|
||||
3 motorcycle
|
||||
4 airplane
|
||||
5 bus
|
||||
6 train
|
||||
7 car
|
||||
8 boat
|
||||
9 traffic light
|
||||
10 fire hydrant
|
||||
12 stop sign
|
||||
13 parking meter
|
||||
14 bench
|
||||
15 bird
|
||||
16 cat
|
||||
17 dog
|
||||
18 horse
|
||||
19 sheep
|
||||
20 cow
|
||||
21 elephant
|
||||
22 bear
|
||||
23 zebra
|
||||
24 giraffe
|
||||
26 backpack
|
||||
27 umbrella
|
||||
30 handbag
|
||||
31 tie
|
||||
32 suitcase
|
||||
33 frisbee
|
||||
34 skis
|
||||
35 snowboard
|
||||
36 sports ball
|
||||
37 kite
|
||||
38 baseball bat
|
||||
39 baseball glove
|
||||
40 skateboard
|
||||
41 surfboard
|
||||
42 tennis racket
|
||||
43 bottle
|
||||
45 wine glass
|
||||
46 cup
|
||||
47 fork
|
||||
48 knife
|
||||
49 spoon
|
||||
50 bowl
|
||||
51 banana
|
||||
52 apple
|
||||
53 sandwich
|
||||
54 orange
|
||||
55 broccoli
|
||||
56 carrot
|
||||
57 hot dog
|
||||
58 pizza
|
||||
59 donut
|
||||
60 cake
|
||||
61 chair
|
||||
62 couch
|
||||
63 potted plant
|
||||
64 bed
|
||||
66 dining table
|
||||
69 toilet
|
||||
71 tv
|
||||
72 laptop
|
||||
73 mouse
|
||||
74 remote
|
||||
75 keyboard
|
||||
76 cell phone
|
||||
77 microwave
|
||||
78 oven
|
||||
79 toaster
|
||||
80 sink
|
||||
81 refrigerator
|
||||
83 book
|
||||
84 clock
|
||||
85 vase
|
||||
86 scissors
|
||||
87 teddy bear
|
||||
88 hair drier
|
||||
89 toothbrush
|
||||
148
process_clip.py
Normal file
148
process_clip.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import sys
|
||||
import click
|
||||
import os
|
||||
import datetime
|
||||
from unittest import TestCase, main
|
||||
from frigate.video import process_frames, start_or_restart_ffmpeg, capture_frames, get_frame_shape
|
||||
from frigate.util import DictFrameManager, EventsPerSecond, draw_box_with_label
|
||||
from frigate.motion import MotionDetector
|
||||
from frigate.edgetpu import LocalObjectDetector
|
||||
from frigate.objects import ObjectTracker
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
import cv2
|
||||
from frigate.object_processing import COLOR_MAP, CameraState
|
||||
|
||||
class ProcessClip():
|
||||
def __init__(self, clip_path, frame_shape, config):
|
||||
self.clip_path = clip_path
|
||||
self.frame_shape = frame_shape
|
||||
self.camera_name = 'camera'
|
||||
self.frame_manager = DictFrameManager()
|
||||
self.frame_queue = mp.Queue()
|
||||
self.detected_objects_queue = mp.Queue()
|
||||
self.camera_state = CameraState(self.camera_name, config, self.frame_manager)
|
||||
|
||||
def load_frames(self):
|
||||
fps = EventsPerSecond()
|
||||
skipped_fps = EventsPerSecond()
|
||||
stop_event = mp.Event()
|
||||
detection_frame = mp.Value('d', datetime.datetime.now().timestamp()+100000)
|
||||
current_frame = mp.Value('d', 0.0)
|
||||
ffmpeg_cmd = f"ffmpeg -hide_banner -loglevel panic -i {self.clip_path} -f rawvideo -pix_fmt rgb24 pipe:".split(" ")
|
||||
ffmpeg_process = start_or_restart_ffmpeg(ffmpeg_cmd, self.frame_shape[0]*self.frame_shape[1]*self.frame_shape[2])
|
||||
capture_frames(ffmpeg_process, self.camera_name, self.frame_shape, self.frame_manager, self.frame_queue, 1, fps, skipped_fps, stop_event, detection_frame, current_frame)
|
||||
ffmpeg_process.wait()
|
||||
ffmpeg_process.communicate()
|
||||
|
||||
def process_frames(self, objects_to_track=['person'], object_filters={}):
|
||||
mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
|
||||
mask[:] = 255
|
||||
motion_detector = MotionDetector(self.frame_shape, mask)
|
||||
|
||||
object_detector = LocalObjectDetector(labels='/labelmap.txt')
|
||||
object_tracker = ObjectTracker(10)
|
||||
process_fps = mp.Value('d', 0.0)
|
||||
detection_fps = mp.Value('d', 0.0)
|
||||
current_frame = mp.Value('d', 0.0)
|
||||
stop_event = mp.Event()
|
||||
|
||||
process_frames(self.camera_name, self.frame_queue, self.frame_shape, self.frame_manager, motion_detector, object_detector, object_tracker, self.detected_objects_queue,
|
||||
process_fps, detection_fps, current_frame, objects_to_track, object_filters, mask, stop_event, exit_on_empty=True)
|
||||
|
||||
def objects_found(self, debug_path=None):
|
||||
obj_detected = False
|
||||
top_computed_score = 0.0
|
||||
def handle_event(name, obj):
|
||||
nonlocal obj_detected
|
||||
nonlocal top_computed_score
|
||||
if obj['computed_score'] > top_computed_score:
|
||||
top_computed_score = obj['computed_score']
|
||||
if not obj['false_positive']:
|
||||
obj_detected = True
|
||||
self.camera_state.on('new', handle_event)
|
||||
self.camera_state.on('update', handle_event)
|
||||
|
||||
while(not self.detected_objects_queue.empty()):
|
||||
camera_name, frame_time, current_tracked_objects = self.detected_objects_queue.get()
|
||||
if not debug_path is None:
|
||||
self.save_debug_frame(debug_path, frame_time, current_tracked_objects.values())
|
||||
|
||||
self.camera_state.update(frame_time, current_tracked_objects)
|
||||
for obj in self.camera_state.tracked_objects.values():
|
||||
print(f"{frame_time}: {obj['id']} - {obj['computed_score']} - {obj['score_history']}")
|
||||
|
||||
return {
|
||||
'object_detected': obj_detected,
|
||||
'top_score': top_computed_score
|
||||
}
|
||||
|
||||
def save_debug_frame(self, debug_path, frame_time, tracked_objects):
|
||||
current_frame = self.frame_manager.get(f"{self.camera_name}{frame_time}")
|
||||
# draw the bounding boxes on the frame
|
||||
for obj in tracked_objects:
|
||||
thickness = 2
|
||||
color = (0,0,175)
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
else:
|
||||
color = (255,255,0)
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj['box']
|
||||
draw_box_with_label(current_frame, box[0], box[1], box[2], box[3], obj['label'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color)
|
||||
# draw the regions on the frame
|
||||
region = obj['region']
|
||||
draw_box_with_label(current_frame, region[0], region[1], region[2], region[3], 'region', "", thickness=1, color=(0,255,0))
|
||||
|
||||
cv2.imwrite(f"{os.path.join(debug_path, os.path.basename(self.clip_path))}.{int(frame_time*1000000)}.jpg", cv2.cvtColor(current_frame, cv2.COLOR_RGB2BGR))
|
||||
|
||||
@click.command()
|
||||
@click.option("-p", "--path", required=True, help="Path to clip or directory to test.")
|
||||
@click.option("-l", "--label", default='person', help="Label name to detect.")
|
||||
@click.option("-t", "--threshold", default=0.85, help="Threshold value for objects.")
|
||||
@click.option("--debug-path", default=None, help="Path to output frames for debugging.")
|
||||
def process(path, label, threshold, debug_path):
|
||||
clips = []
|
||||
if os.path.isdir(path):
|
||||
files = os.listdir(path)
|
||||
files.sort()
|
||||
clips = [os.path.join(path, file) for file in files]
|
||||
elif os.path.isfile(path):
|
||||
clips.append(path)
|
||||
|
||||
config = {
|
||||
'snapshots': {
|
||||
'show_timestamp': False,
|
||||
'draw_zones': False
|
||||
},
|
||||
'zones': {},
|
||||
'objects': {
|
||||
'track': [label],
|
||||
'filters': {
|
||||
'person': {
|
||||
'threshold': threshold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = []
|
||||
for c in clips:
|
||||
frame_shape = get_frame_shape(c)
|
||||
process_clip = ProcessClip(c, frame_shape, config)
|
||||
process_clip.load_frames()
|
||||
process_clip.process_frames(objects_to_track=config['objects']['track'])
|
||||
|
||||
results.append((c, process_clip.objects_found(debug_path)))
|
||||
|
||||
for result in results:
|
||||
print(f"{result[0]}: {result[1]}")
|
||||
|
||||
positive_count = sum(1 for result in results if result[1]['object_detected'])
|
||||
print(f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s).")
|
||||
|
||||
if __name__ == '__main__':
|
||||
process()
|
||||
Reference in New Issue
Block a user