Compare commits

...

40 Commits

Author SHA1 Message Date
Blake Blackshear
3ad75a441d remove redundant error output 2020-12-20 08:04:54 -06:00
Blake Blackshear
f006e9be8d use CACHE_DIR constant 2020-12-20 08:04:54 -06:00
Blake Blackshear
03f3ba8008 enable mounting tmpfs volume on start 2020-12-20 08:04:54 -06:00
Blake Blackshear
96a44eb7bf docs and issue template 2020-12-20 07:37:44 -06:00
Blake Blackshear
006782fe3d update process clip for latest changes 2020-12-20 07:37:44 -06:00
Blake Blackshear
ff3e95bbf7 publish event updates on zone change 2020-12-20 07:37:44 -06:00
Blake Blackshear
4b95a37e65 readme updates 2020-12-20 07:37:44 -06:00
Blake Blackshear
38c661b3a8 handle scenario with empty cache 2020-12-20 07:37:44 -06:00
Blake Blackshear
0d6e4f6a66 add qsv support to amd64 image 2020-12-20 07:37:44 -06:00
Blake Blackshear
1ad2219f1c add num_threads fixes #322 2020-12-20 07:37:44 -06:00
Blake Blackshear
dfcdd289c3 optimize clips fixes #299 2020-12-20 07:37:44 -06:00
Blake Blackshear
32f5f2cca9 add post_capture option 2020-12-20 07:37:44 -06:00
Blake Blackshear
24bfe9f3e8 re-crop to the object rather than the region 2020-12-20 07:37:44 -06:00
Blake Blackshear
004667dc99 allow runtime drawing settings for mjpeg and latest 2020-12-20 07:37:44 -06:00
Blake Blackshear
9d785dc781 allow the mask to be a list of masks 2020-12-20 07:37:44 -06:00
Blake Blackshear
cbba5a7af0 adding version endpoint 2020-12-20 07:37:44 -06:00
Blake Blackshear
29b29ee349 configurable motion and detect settings 2020-12-20 07:37:44 -06:00
Blake Blackshear
9ad53e09af update gitignore 2020-12-20 07:37:44 -06:00
Blake Blackshear
c9278991c9 fix test 2020-12-20 07:37:44 -06:00
Blake Blackshear
729de48934 switch default threshold to .7 2020-12-20 07:37:44 -06:00
Blake Blackshear
7476bff5fb allow process clips to output a csv of scores 2020-12-20 07:37:44 -06:00
Blake Blackshear
1e9eae8d9a allow db path to be customized 2020-12-20 07:37:44 -06:00
Blake Blackshear
8113a53381 add telegram example 2020-12-20 07:37:44 -06:00
Blake Blackshear
72833686f1 fix process clip 2020-12-20 07:37:44 -06:00
Blake Blackshear
096c21f105 handle empty string args 2020-12-20 07:37:44 -06:00
Blake Blackshear
181f66357b allow region to extend beyond the frame 2020-12-20 07:37:44 -06:00
tubalainen
a54fbc483c Updated file
ref: https://github.com/blakeblackshear/frigate/issues/373
2020-12-12 10:38:02 -06:00
Blake Blackshear
92d5a002d3 swap width and height to reduce confusion 2020-12-10 19:22:03 -06:00
Blake Blackshear
f9184903d7 updating compose example to reduce confusion 2020-12-10 19:02:08 -06:00
Blake Blackshear
91cde6ce7b allow defining model shape and switch to mobiledet as default model 2020-12-09 07:22:26 -06:00
Blake Blackshear
186a4587c7 add model dimensions to config 2020-12-09 07:22:26 -06:00
Patrick Decat
6049acb1f3 Document beta addon host 2020-12-08 07:25:13 -06:00
Blake Blackshear
2d2ebf313c make shm consistent with compose 2020-12-08 07:24:37 -06:00
tubalainen
3d329dcb52 Updated docker command line...
...to correspond with 0.8.0 feature set.
2020-12-08 07:24:37 -06:00
Blake Blackshear
06854fc34f readme cleanup fixes #332 2020-12-07 18:00:12 -06:00
Blake Blackshear
e01e14d866 handle and warn if roles dont match enabled features 2020-12-07 08:07:35 -06:00
Blake Blackshear
3dfd251ebb camera recommendations 2020-12-07 07:36:29 -06:00
Blake Blackshear
dcea807f77 catch all psutil errors 2020-12-07 07:16:48 -06:00
Blake Blackshear
87d83ff33a clarify height width and fps 2020-12-07 07:16:28 -06:00
Blake Blackshear
1d31cbdf0d readme updates 2020-12-06 14:25:28 -06:00
23 changed files with 1019 additions and 418 deletions

View File

@@ -3,4 +3,5 @@ docs/
.gitignore
debug
config/
*.pyc
*.pyc
.git

View File

@@ -1,6 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
name: Bug report or Support request
about: ''
title: ''
labels: ''
assignees: ''
@@ -8,10 +8,10 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
A clear and concise description of what your issue is.
**Version of frigate**
What version are you using?
Output from `/version`
**Config file**
Include your full config file wrapped in triple back ticks.
@@ -19,14 +19,14 @@ Include your full config file wrapped in triple back ticks.
config here
```
**Logs**
**Frigate container logs**
```
Include relevant log output here
```
**Frigate debug stats**
```
Output from frigate's /debug/stats endpoint
**Frigate stats**
```json
Output from frigate's /stats endpoint
```
**FFprobe from your camera**
@@ -41,6 +41,7 @@ If applicable, add screenshots to help explain your problem.
**Computer Hardware**
- OS: [e.g. Ubuntu, Windows]
- Install method: [e.g. Addon, Docker Compose, Docker Command]
- Virtualization: [e.g. Proxmox, Virtualbox]
- Coral Version: [e.g. USB, PCIe, None]
- Network Setup: [e.g. Wired, WiFi]

6
.gitignore vendored
View File

@@ -1,4 +1,8 @@
*.pyc
debug
.vscode
config/config.yml
config/config.yml
models
*.mp4
*.db
frigate/version.py

View File

@@ -1,13 +1,18 @@
default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h")
version:
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
amd64_wheels:
docker build --tag blakeblackshear/frigate-wheels:amd64 --file docker/Dockerfile.wheels .
amd64_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
amd64_frigate:
docker build --tag frigate-base --build-arg ARCH=amd64 --file docker/Dockerfile.base .
amd64_frigate: version
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.amd64 .
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
@@ -18,8 +23,8 @@ amd64nvidia_wheels:
amd64nvidia_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
amd64nvidia_frigate:
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --file docker/Dockerfile.base .
amd64nvidia_frigate: version
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
@@ -30,8 +35,8 @@ aarch64_wheels:
aarch64_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate:
docker build --tag frigate-base --build-arg ARCH=aarch64 --file docker/Dockerfile.base .
aarch64_frigate: version
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.aarch64 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
@@ -42,8 +47,8 @@ armv7_wheels:
armv7_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
armv7_frigate:
docker build --tag frigate-base --build-arg ARCH=armv7 --file docker/Dockerfile.base .
armv7_frigate: version
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.armv7 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate

205
README.md
View File

@@ -33,9 +33,9 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
- [Object Filters](#object-filters)
- [Masks](#masks)
- [Zones](#zones)
- [Recording Clips](#recording-clips)
- [24/7 Recordings](#247-recordings)
- [RTMP Streams](#rtmp-streams)
- [Recording Clips (save_clips)](#recording-clips)
- [24/7 Recordings (record)](#247-recordings)
- [RTMP Streams (rtmp)](#rtmp-streams)
- [Integration with HomeAssistant](#integration-with-homeassistant)
- [MQTT Topics](#mqtt-topics)
- [HTTP Endpoints](#http-endpoints)
@@ -43,6 +43,11 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
- [Troubleshooting](#troubleshooting)
## Recommended Hardware
### Cameras
Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and HomeAssistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, clips, and recordings without re-encoding.
### Computer
|Name|Inference Speed|Notes|
|----|---------------|-----|
|Atomic Pi|16ms|Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding.|
@@ -56,9 +61,14 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
[Back to top](#documentation)
## Installing
Frigate is a Docker container that can be run on any Docker host including as a [HassOS Addon](https://www.home-assistant.io/addons/). See instructions below for installing the HassOS addon.
For HomeAssistant users, there is also a [custom component (aka integration)](https://github.com/blakeblackshear/frigate-hass-integration). This custom component adds tighter integration with HomeAssistant by automatically setting up camera entities, sensors, media browser for clips and recordings, and a public API to simplify notifications.
Note that HassOS Addons and custom components are different things. If you are already running Frigate with Docker directly, you do not need the Addon since the Addon would run another instance of Frigate.
### HassOS Addon
HassOS users can install via the addon repository. Frigate requires that an MQTT server be running.
HassOS users can install via the addon repository. Frigate requires an MQTT server.
1. Navigate to Supervisor > Add-on Store > Repositories
1. Add https://github.com/blakeblackshear/frigate-hass-addons
1. Setup your configuration in the `Configuration` tab
@@ -69,16 +79,19 @@ Make sure you choose the right image for your architecture:
|Arch|Image Name|
|-|-|
|amd64|blakeblackshear/frigate:stable-amd64|
|amd64nvidia|blakeblackshear/frigate:stable-amd64nvidia|
|armv7|blakeblackshear/frigate:stable-armv7|
|aarch64|blakeblackshear/frigate:stable-aarch64|
It is recommended to run with docker-compose:
```yaml
version: "3.6"
services:
frigate:
container_name: frigate
restart: unless-stopped
privileged: true
image: blakeblackshear/frigate:stable-amd64
image: blakeblackshear/frigate:0.8.0-beta2-amd64
volumes:
- /dev/bus/usb:/dev/bus/usb
- /etc/localtime:/etc/localtime:ro
@@ -94,12 +107,6 @@ It is recommended to run with docker-compose:
- "1935:1935" # RTMP feeds
environment:
FRIGATE_RTSP_PASSWORD: "password"
healthcheck:
test: ["CMD", "wget" , "-q", "-O-", "http://localhost:5000"]
interval: 30s
timeout: 10s
retries: 5
start_period: 3m
```
If you can't use docker compose, you can run the container with something similar to this:
@@ -107,12 +114,16 @@ If you can't use docker compose, you can run the container with something simila
docker run --rm \
--name frigate \
--privileged \
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=100000000 \
-v /dev/bus/usb:/dev/bus/usb \
-v <path_to_config_dir>:/config:ro \
-v <path_to_directory_for_clips>:/media/frigate/clips \
-v <path_to_directory_for_recordings>:/media/frigate/recordings \
-v <path_to_config>:/config:ro \
-v /etc/localtime:/etc/localtime:ro \
-p 5000:5000 \
-e FRIGATE_RTSP_PASSWORD='password' \
blakeblackshear/frigate:stable-amd64
-p 5000:5000 \
-p 1935:1935 \
blakeblackshear/frigate:0.8.0-beta2-amd64
```
### Kubernetes
@@ -165,8 +176,8 @@ cameras:
roles:
- detect
- rtmp
height: 720
width: 1280
height: 720
fps: 5
```
Here are all the configuration options:
@@ -179,6 +190,12 @@ logger:
logs:
frigate.mqtt: error
# Optional: database configuration
database:
# Optional: database path
# This may need to be in a custom location if network storage is used for clips
path: /media/frigate/clips/frigate.db
# Optional: detectors configuration
# USB Coral devices will be auto detected with CPU fallback
detectors:
@@ -189,6 +206,16 @@ detectors:
type: edgetpu
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
device: usb
# Optional: num_threads value passed to the tflite.Interpreter (default: shown below)
# This value is only used for CPU types
num_threads: 3
# Optional: model configuration
model:
# Required: height of the trained model
height: 320
# Required: width of the trained model
width: 320
# Required: mqtt configuration
mqtt:
@@ -215,6 +242,10 @@ save_clips:
# NOTE: 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
# Optional: size of tmpfs mount to create for cache files (default: not set)
# mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
# Notice: If you have mounted a tmpfs volume through docker, this value should not be set in your config
tmpfs_cache_size: 256m
# Optional: Retention settings for clips (default: shown below)
retain:
# Required: Default retention days (default: shown below)
@@ -261,7 +292,39 @@ objects:
# Optional: minimum score for the object to initiate tracking (default: shown below)
min_score: 0.5
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
threshold: 0.85
threshold: 0.7
# Optional: Global motion detection config. These may also be defined at the camera level.
# ADVANCED: Most users will not need to set these values in their config
motion:
# Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
# The value should be between 1 and 255.
threshold: 25
# Optional: Minimum size in pixels in the resized motion image that counts as motion
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller
# moving objects.
contour_area: 100
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
# Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
# Too low and a fast moving person wont be detected as motion.
delta_alpha: 0.2
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
# Low values will cause things like moving shadows to be detected as motion for longer.
# https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
frame_alpha: 0.2
# Optional: Height of the resized motion frame (default: 1/6th of the original frame height)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage.
# Lower values result in less CPU, but small changes may not register as motion.
frame_height: 180
# Optional: Global detecttion settings. These may also be defined at the camera level.
# ADVANCED: Most users will not need to set these values in their config
detect:
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: double the frame rate)
max_disappeared: 10
# Required: configuration section for cameras
cameras:
@@ -275,6 +338,8 @@ cameras:
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
# Required: list of roles for this stream. valid values are: detect,record,clips,rtmp
# NOTICE: In addition to assigning the record, clips, and rtmp roles,
# they must also be enabled in the camera config.
roles:
- detect
- rtmp
@@ -294,32 +359,24 @@ cameras:
# Optional: camera specific output args (default: inherit)
output_args:
# Required: height of the frame
# NOTE: Recommended to set this value, but frigate will attempt to autodetect.
height: 720
# Required: width of the frame
# NOTE: Recommended to set this value, but frigate will attempt to autodetect.
# Required: width of the frame for the input with the detect role
width: 1280
# Optional: desired fps for your camera
# Required: height of the frame for the input with the detect role
height: 720
# Optional: desired fps for your camera for the input with the detect role
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
# Frigate will attempt to autodetect if not specified.
fps: 5
# Optional: motion mask
# Optional: list of motion masks
# NOTE: see docs for more detailed info on creating masks
mask: poly,0,900,1080,900,1080,1920,0,1920
mask:
- poly,0,900,1080,900,1080,1920,0,1920
# Optional: timeout for highest scoring image before allowing it
# to be replaced by a newer image. (default: shown below)
best_image_timeout: 60
# Optional: camera specific mqtt settings
mqtt:
# Optional: crop the camera frame to the detection region of the object (default: False)
crop_to_region: True
# Optional: resize the image before publishing over mqtt
snapshot_height: 175
# Optional: zones for this camera
zones:
# Required: name of the zone
@@ -335,7 +392,7 @@ cameras:
person:
min_area: 5000
max_area: 100000
threshold: 0.8
threshold: 0.7
# Optional: save clips configuration
# NOTE: This feature does not work if you have added "-vsync drop" in your input params.
@@ -345,7 +402,9 @@ cameras:
# Required: enables clips for the camera (default: shown below)
enabled: False
# Optional: Number of seconds before the event to include in the clips (default: shown below)
pre_capture: 30
pre_capture: 5
# Optional: Number of seconds after the event to include in the clips (default: shown below)
post_capture: 5
# Optional: Objects to save clips for. (default: all tracked objects)
objects:
- person
@@ -393,7 +452,7 @@ cameras:
min_area: 5000
max_area: 100000
min_score: 0.5
threshold: 0.85
threshold: 0.7
```
[Back to top](#documentation)
@@ -424,8 +483,8 @@ cameras:
roles:
- clips
- record
height: 720
width: 1280
height: 720
fps: 5
```
@@ -433,7 +492,7 @@ cameras:
[Back to top](#documentation)
## Optimizing Performance
- **Google Coral**: It is strongly recommended to use a Google Coral, but Frigate will fall back to CPU in the event one is not found. Offloading TensorFlow to the Google Coral is an order of magnitude faster and will reduce your CPU load dramatically. A $60 device will outperform $2000 CPU.
- **Google Coral**: It is strongly recommended to use a Google Coral, but Frigate will fall back to CPU in the event one is not found. Offloading TensorFlow to the Google Coral is an order of magnitude faster and will reduce your CPU load dramatically. A $60 device will outperform $2000 CPU. Frigate should work with any supported Coral device from https://coral.ai
- **Resolution**: For the `detect` input, choose a camera resolution where the smallest object you want to detect barely fits inside a 300x300px square. The model used by Frigate is trained on 300x300px images, so you will get worse performance and no improvement in accuracy by using a larger resolution since Frigate resizes the area where it is looking for objects to 300x300 anyway.
- **FPS**: 5 frames per second should be adequate. Higher frame rates will require more CPU usage without improving detections or accuracy. Reducing the frame rate on your camera will have the greatest improvement on system resources.
- **Hardware Acceleration**: Make sure you configure the `hwaccel_args` for your hardware. They provide a significant reduction in CPU usage if they are available.
@@ -442,7 +501,8 @@ cameras:
### FFmpeg Hardware Acceleration
Frigate works on Raspberry Pi 3b/4 and x86 machines. It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible.
Raspberry Pi 3/4 (32-bit OS):
Raspberry Pi 3/4 (32-bit OS)
**NOTICE**: If you are using the addon, ensure you turn off `Protection mode` for hardware acceleration.
```yaml
ffmpeg:
hwaccel_args:
@@ -451,6 +511,7 @@ ffmpeg:
```
Raspberry Pi 3/4 (64-bit OS)
**NOTICE**: If you are using the addon, ensure you turn off `Protection mode` for hardware acceleration.
```yaml
ffmpeg:
hwaccel_args:
@@ -471,7 +532,17 @@ ffmpeg:
```
Intel-based CPUs (>=10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
**Note:** You also need to set `LIBVA_DRIVER_NAME=iHD` as an environment variable on the container.
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- qsv
- -qsv_device
- /dev/dri/renderD128
```
AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
**Note:** You also need to set `LIBVA_DRIVER_NAME=radeonsi` as an environment variable on the container.
```yaml
ffmpeg:
hwaccel_args:
@@ -490,6 +561,8 @@ By default Frigate will look for a USB Coral device and fall back to the CPU if
Frigate supports `edgetpu` and `cpu` as detector types. The device value should be specified according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api).
**Note**: There is no support for Nvidia GPUs to perform object detection with tensorflow. It can be used for ffmpeg decoding, but not object detection.
Single USB Coral:
```yaml
detectors:
@@ -597,11 +670,14 @@ Frigate can save video clips without any CPU overhead for encoding by simply cop
### Database
Event and clip information is managed in a sqlite database at `/media/frigate/clips/frigate.db`. If that database is deleted, clips will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within HomeAssistant.
If you are storing your clips on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary.
### 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.
- `pre_capture`: Defines how much time should be included in the clip prior to the beginning of the event. Defaults to 5 seconds.
- `post_capture`: Defines how much time should be included in the clip after the end of the event. Defaults to 5 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.
@@ -615,12 +691,14 @@ Event and clip information is managed in a sqlite database at `/media/frigate/cl
[Back to top](#documentation)
## RTMP Streams
Frigate can re-stream your video feed as a RTMP feed for other applications such as HomeAssistant to utilize it. This allows you to use a video feed for detection in frigate and HomeAssistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Frigate can re-stream your video feed as a RTMP feed for other applications such as HomeAssistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and HomeAssistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization.
[Back to top](#documentation)
## Integration with HomeAssistant
The best way to integrate with HomeAssistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). When configuring the integration, you will be asked for the `Host` of your frigate instance. This value should be the url you use to access Frigate in the browser and will look like `http://<host>:5000/`. If you are using HassOS with the addon, the host should be `http://ccab4aaf-frigate:5000`. HomeAssistant needs access to port 5000 (api) and 1935 (rtmp) for all features. The integration will setup the following entities within HomeAssistant:
The best way to integrate with HomeAssistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). When configuring the integration, you will be asked for the `Host` of your frigate instance. This value should be the url you use to access Frigate in the browser and will look like `http://<host>:5000/`. If you are using HassOS with the addon, the host should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). HomeAssistant needs access to port 5000 (api) and 1935 (rtmp) for all features. The integration will setup the following entities within HomeAssistant:
Sensors:
- Stats to monitor frigate performance
@@ -667,7 +745,19 @@ A web server is available on port 5000 with the following endpoints.
### `/<camera_name>`
An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging only and will put additional load on the system when in use.
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`
Accepts the following query string parameters:
|param|Type|Description|
|----|-----|--|
|`fps`|int|Frame rate|
|`h`|int|Height in pixels|
|`bbox`|int|Show bounding boxes for detected objects (0 or 1)|
|`timestamp`|int|Print the timestamp in the upper left (0 or 1)|
|`zones`|int|Draw the zones on the image (0 or 1)|
|`mask`|int|Overlay the mask on the image (0 or 1)|
|`motion`|int|Draw blue boxes for areas with detected motion (0 or 1)|
|`regions`|int|Draw green boxes for areas where object detection was run (0 or 1)|
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[?h=300&crop=1]`
The best snapshot for any object type. It is a full resolution image by default.
@@ -679,6 +769,17 @@ Example parameters:
### `/<camera_name>/latest.jpg[?h=300]`
The most recent frame that frigate has finished processing. It is a full resolution image by default.
Accepts the following query string parameters:
|param|Type|Description|
|----|-----|--|
|`h`|int|Height in pixels|
|`bbox`|int|Show bounding boxes for detected objects (0 or 1)|
|`timestamp`|int|Print the timestamp in the upper left (0 or 1)|
|`zones`|int|Draw the zones on the image (0 or 1)|
|`mask`|int|Overlay the mask on the image (0 or 1)|
|`motion`|int|Draw blue boxes for areas with detected motion (0 or 1)|
|`regions`|int|Draw green boxes for areas where object detection was run (0 or 1)|
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
@@ -748,6 +849,9 @@ Sample response:
### `/config`
A json representation of your configuration
### `/version`
Version info
### `/events`
Events from the database. Accepts the following query string parameters:
|param|Type|Description|
@@ -790,8 +894,9 @@ is published again.
The height and crop of snapshots can be configured in the config.
### `frigate/events`
Message published for each changed event:
```json
Message published for each changed event. The first message is published when the tracked object is no longer marked as a false_positive. When frigate finds a better snapshot of the tracked object or when a zone change occurs, it will publish a message with the same id. When the event ends, a final message is published with `end_time` set.
```jsonc
{
"before": {
"id": "1607123955.475377-mxklsc",
@@ -869,6 +974,8 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
You also need to update the model width/height in the config if they differ from the defaults.
### Customizing the Labelmap
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. You must retain the same number of labels, but you can change the names. To change:
@@ -895,14 +1002,14 @@ Examples of available modules are:
## Troubleshooting
### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
### "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`:
Turn on logging for the ffmpeg process by overriding the global_args and setting the log level to `info` (the default is `fatal`). Note that all ffmpeg logs show up in the Frigate logs as `ERROR` level. This does not mean they are actually errors.
```yaml
ffmpeg:
global_args:
- -hide_banner
- -loglevel
- info
global_args: -hide_banner -loglevel info
```
### "On connect called"

View File

@@ -9,7 +9,7 @@ RUN apt-get -qq update \
# ffmpeg dependencies
libgomp1 \
# VAAPI drivers for Intel hardware accel
libva-drm2 libva2 i965-va-driver vainfo intel-media-va-driver mesa-va-drivers \
libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver mesa-va-drivers \
## Tensorflow lite
&& wget -q https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
&& python3.8 -m pip install tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \

View File

@@ -1,6 +1,7 @@
ARG ARCH=amd64
ARG FFMPEG_VERSION
FROM blakeblackshear/frigate-wheels:${ARCH} as wheels
FROM blakeblackshear/frigate-ffmpeg:1.0.0-${ARCH} as ffmpeg
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
FROM ubuntu:20.04
LABEL maintainer "blakeb@blakeshome.com"
@@ -36,10 +37,9 @@ RUN pip3 install \
COPY nginx/nginx.conf /etc/nginx/nginx.conf
# get model and labels
ARG MODEL_REFS=7064b94dd5b996189242320359dbab8b52c94a84
COPY labelmap.txt /labelmap.txt
RUN wget -q https://github.com/google-coral/edgetpu/raw/$MODEL_REFS/test_data/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/edgetpu/raw/$MODEL_REFS/test_data/ssd_mobilenet_v2_coco_quant_postprocess.tflite -O /cpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
WORKDIR /opt/frigate/
ADD frigate frigate/

View File

@@ -79,6 +79,7 @@ RUN buildDeps="autoconf \
libssl-dev \
yasm \
libva-dev \
libmfx-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
@@ -404,6 +405,7 @@ RUN \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmfx \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \

View File

@@ -1,5 +1,6 @@
# Notification examples
Here are some examples of notifications for the HomeAssistant android companion app:
```yaml
automation:
@@ -8,45 +9,63 @@ automation:
platform: mqtt
topic: frigate/events
conditions:
- "{{ trigger.payload_json["after"]["label"] == 'person' }}"
- "{{ 'yard' in trigger.payload_json["after"]["entered_zones"] }}"
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} has entered the yard.'
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
data:
image: 'https://url.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}.jpg'
tag: '{{trigger.payload_json["after"]["id"]}}'
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
- alias: When a person leaves a zone named yard
trigger:
platform: mqtt
topic: frigate/events
conditions:
- "{{ trigger.payload_json["after"]["label"] == 'person' }}"
- "{{ 'yard' in trigger.payload_json["before"]["current_zones"] }}"
- "{{ not 'yard' in trigger.payload_json["after"]["current_zones"] }}"
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} has left the yard.'
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
data:
image: 'https://url.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}.jpg'
tag: '{{trigger.payload_json["after"]["id"]}}'
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
- alias: Notify for dogs in the front with a high top score
trigger:
platform: mqtt
topic: frigate/events
conditions:
- "{{ trigger.payload_json["after"]["label"] == 'dog' }}"
- "{{ trigger.payload_json["after"]["camera"] == 'front' }}"
- "{{ trigger.payload_json["after"]["top_score"] > 0.98 }}"
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'High confidence dog detection.'
data:
image: 'https://url.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}.jpg'
tag: '{{trigger.payload_json["after"]["id"]}}'
```
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
```
If you are using telegram, you can fetch the image directly from Frigate:
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.telegram_full
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
photo:
# this url should work for addon users
- url: 'http://ccab4aaf-frigate:5000/events/{{trigger.payload_json["after"]["id"]}}/snapshot.jpg'
caption : 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
```

View File

@@ -38,6 +38,13 @@ class FrigateApp():
self.camera_metrics = {}
def ensure_dirs(self):
tmpfs_size = self.config.save_clips.tmpfs_cache_size
if tmpfs_size:
logger.info(f"Creating tmpfs of size {tmpfs_size}")
rc = os.system(f"mount -t tmpfs -o size={tmpfs_size} tmpfs {CACHE_DIR}")
if rc != 0:
logger.error(f"Failed to create tmpfs, error code: {rc}")
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
if not os.path.exists(d) and not os.path.islink(d):
logger.info(f"Creating directory: {d}")
@@ -67,6 +74,24 @@ class FrigateApp():
'ffmpeg_pid': mp.Value('i', 0),
'frame_queue': mp.Queue(maxsize=2)
}
def check_config(self):
for name, camera in self.config.cameras.items():
assigned_roles = list(set([r for i in camera.ffmpeg.inputs for r in i.roles]))
if not camera.save_clips.enabled and 'clips' in assigned_roles:
logger.warning(f"Camera {name} has clips assigned to an input, but save_clips is not enabled.")
elif camera.save_clips.enabled and not 'clips' in assigned_roles:
logger.warning(f"Camera {name} has save_clips enabled, but clips is not assigned to an input.")
if not camera.record.enabled and 'record' in assigned_roles:
logger.warning(f"Camera {name} has record assigned to an input, but record is not enabled.")
elif camera.record.enabled and not 'record' in assigned_roles:
logger.warning(f"Camera {name} has record enabled, but record is not assigned to an input.")
if not camera.rtmp.enabled and 'rtmp' in assigned_roles:
logger.warning(f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled.")
elif camera.rtmp.enabled and not 'rtmp' in assigned_roles:
logger.warning(f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input.")
def set_log_levels(self):
logging.getLogger().setLevel(self.config.logger.default)
@@ -85,7 +110,7 @@ class FrigateApp():
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
def init_database(self):
self.db = SqliteExtDatabase(f"/{os.path.join(CLIPS_DIR, 'frigate.db')}")
self.db = SqliteExtDatabase(self.config.database.path)
models = [Event]
self.db.bind(models)
self.db.create_tables(models, safe=True)
@@ -97,18 +122,19 @@ class FrigateApp():
self.mqtt_client = create_mqtt_client(self.config.mqtt)
def start_detectors(self):
model_shape = (self.config.model.height, self.config.model.width)
for name in self.config.cameras.keys():
self.detection_out_events[name] = mp.Event()
shm_in = mp.shared_memory.SharedMemory(name=name, create=True, size=300*300*3)
shm_in = mp.shared_memory.SharedMemory(name=name, create=True, size=self.config.model.height*self.config.model.width*3)
shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}", create=True, size=20*6*4)
self.detection_shms.append(shm_in)
self.detection_shms.append(shm_out)
for name, detector in self.config.detectors.items():
if detector.type == 'cpu':
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, out_events=self.detection_out_events, tf_device='cpu')
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, 'cpu', detector.num_threads)
if detector.type == 'edgetpu':
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, out_events=self.detection_out_events, tf_device=detector.device)
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, detector.device, detector.num_threads)
def start_detected_frames_processor(self):
self.detected_frames_processor = TrackedObjectProcessor(self.config, self.mqtt_client, self.config.mqtt.topic_prefix,
@@ -116,8 +142,9 @@ class FrigateApp():
self.detected_frames_processor.start()
def start_camera_processors(self):
model_shape = (self.config.model.height, self.config.model.width)
for name, config in self.config.cameras.items():
camera_process = mp.Process(target=track_camera, name=f"camera_processor:{name}", args=(name, config,
camera_process = mp.Process(target=track_camera, name=f"camera_processor:{name}", args=(name, config, model_shape,
self.detection_queue, self.detection_out_events[name], self.detected_frames_queue,
self.camera_metrics[name]))
camera_process.daemon = True
@@ -153,19 +180,20 @@ class FrigateApp():
def start(self):
self.init_logger()
try:
self.ensure_dirs()
try:
self.init_config()
except Exception as e:
logger.error(f"Error parsing config: {e}")
self.log_process.terminate()
sys.exit(1)
self.ensure_dirs()
self.check_config()
self.set_log_levels()
self.init_queues()
self.init_database()
self.init_mqtt()
except Exception as e:
logger.error(e)
print(e)
self.log_process.terminate()
sys.exit(1)
self.start_detectors()

View File

@@ -15,7 +15,8 @@ DETECTORS_SCHEMA = vol.Schema(
{
vol.Required(str): {
vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
vol.Optional('device', default='usb'): str
vol.Optional('device', default='usb'): str,
vol.Optional('num_threads', default=3): int
}
}
)
@@ -50,6 +51,7 @@ SAVE_CLIPS_RETAIN_SCHEMA = vol.Schema(
SAVE_CLIPS_SCHEMA = vol.Schema(
{
vol.Optional('max_seconds', default=300): int,
'tmpfs_cache_size': str,
vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA
}
)
@@ -84,12 +86,28 @@ GLOBAL_FFMPEG_SCHEMA = vol.Schema(
}
)
MOTION_SCHEMA = vol.Schema(
{
'threshold': vol.Range(min=1, max=255),
'contour_area': int,
'delta_alpha': float,
'frame_alpha': float,
'frame_height': int
}
)
DETECT_SCHEMA = vol.Schema(
{
'max_disappeared': int
}
)
FILTER_SCHEMA = vol.Schema(
{
str: {
vol.Optional('min_area', default=0): int,
vol.Optional('max_area', default=24000000): int,
vol.Optional('threshold', default=0.85): float
vol.Optional('threshold', default=0.7): float
}
}
)
@@ -109,16 +127,6 @@ OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
}
))
DEFAULT_CAMERA_SAVE_CLIPS = {
'enabled': False
}
DEFAULT_CAMERA_SNAPSHOTS = {
'show_timestamp': True,
'draw_zones': False,
'draw_bounding_boxes': True,
'crop_to_region': True
}
def each_role_used_once(inputs):
roles = [role for i in inputs for role in i['roles']]
roles_set = set(roles)
@@ -158,7 +166,7 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Required('height'): int,
vol.Required('width'): int,
'fps': int,
'mask': str,
'mask': vol.Any(str, [str]),
vol.Optional('best_image_timeout', default=60): int,
vol.Optional('zones', default={}): {
str: {
@@ -166,9 +174,10 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('filters', default={}): FILTER_SCHEMA
}
},
vol.Optional('save_clips', default=DEFAULT_CAMERA_SAVE_CLIPS): {
vol.Optional('save_clips', default={}): {
vol.Optional('enabled', default=False): bool,
vol.Optional('pre_capture', default=30): int,
vol.Optional('pre_capture', default=5): int,
vol.Optional('post_capture', default=5): int,
'objects': [str],
vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA,
},
@@ -179,20 +188,29 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('rtmp', default={}): {
vol.Required('enabled', default=True): bool,
},
vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): {
vol.Optional('snapshots', default={}): {
vol.Optional('show_timestamp', default=True): bool,
vol.Optional('draw_zones', default=False): bool,
vol.Optional('draw_bounding_boxes', default=True): bool,
vol.Optional('crop_to_region', default=True): bool,
vol.Optional('height', default=175): int
},
'objects': OBJECTS_SCHEMA
'objects': OBJECTS_SCHEMA,
vol.Optional('motion', default={}): MOTION_SCHEMA,
vol.Optional('detect', default={}): DETECT_SCHEMA
}
}, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
)
FRIGATE_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional('database', default={}): {
vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
},
vol.Optional('model', default={'width': 320, 'height': 320}): {
vol.Required('width'): int,
vol.Required('height'): int
},
vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
'mqtt': MQTT_SCHEMA,
vol.Optional('logger', default={'default': 'info', 'logs': {}}): {
@@ -206,14 +224,49 @@ FRIGATE_CONFIG_SCHEMA = vol.Schema(
},
vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
vol.Optional('objects', default={}): OBJECTS_SCHEMA,
vol.Optional('motion', default={}): MOTION_SCHEMA,
vol.Optional('detect', default={}): DETECT_SCHEMA,
vol.Required('cameras', default={}): CAMERAS_SCHEMA
}
)
class DatabaseConfig():
def __init__(self, config):
self._path = config['path']
@property
def path(self):
return self._path
def to_dict(self):
return {
'path': self.path
}
class ModelConfig():
def __init__(self, config):
self._width = config['width']
self._height = config['height']
@property
def width(self):
return self._width
@property
def height(self):
return self._height
def to_dict(self):
return {
'width': self.width,
'height': self.height
}
class DetectorConfig():
def __init__(self, config):
self._type = config['type']
self._device = config['device']
self._num_threads = config['num_threads']
@property
def type(self):
@@ -223,10 +276,15 @@ class DetectorConfig():
def device(self):
return self._device
@property
def num_threads(self):
return self._num_threads
def to_dict(self):
return {
'type': self.type,
'device': self.device
'device': self.device,
'num_threads': self.num_threads
}
class LoggerConfig():
@@ -353,11 +411,16 @@ class SaveClipsRetainConfig():
class SaveClipsConfig():
def __init__(self, config):
self._max_seconds = config['max_seconds']
self._tmpfs_cache_size = config.get('tmpfs_cache_size', '').strip()
self._retain = SaveClipsRetainConfig(config['retain'], config['retain'])
@property
def max_seconds(self):
return self._max_seconds
@property
def tmpfs_cache_size(self):
return self._tmpfs_cache_size
@property
def retain(self):
@@ -366,6 +429,7 @@ class SaveClipsConfig():
def to_dict(self):
return {
'max_seconds': self.max_seconds,
'tmpfs_cache_size': self.tmpfs_cache_size,
'retain': self.retain.to_dict()
}
@@ -482,6 +546,7 @@ class CameraSaveClipsConfig():
def __init__(self, global_config, config):
self._enabled = config['enabled']
self._pre_capture = config['pre_capture']
self._post_capture = config['post_capture']
self._objects = config.get('objects', global_config['objects']['track'])
self._retain = SaveClipsRetainConfig(global_config['save_clips']['retain'], config['retain'])
@@ -492,6 +557,10 @@ class CameraSaveClipsConfig():
@property
def pre_capture(self):
return self._pre_capture
@property
def post_capture(self):
return self._post_capture
@property
def objects(self):
@@ -505,6 +574,7 @@ class CameraSaveClipsConfig():
return {
'enabled': self.enabled,
'pre_capture': self.pre_capture,
'post_capture': self.post_capture,
'objects': self.objects,
'retain': self.retain.to_dict()
}
@@ -522,6 +592,58 @@ class CameraRtmpConfig():
'enabled': self.enabled,
}
class MotionConfig():
def __init__(self, global_config, config, camera_height: int):
self._threshold = config.get('threshold', global_config.get('threshold', 25))
self._contour_area = config.get('contour_area', global_config.get('contour_area', 100))
self._delta_alpha = config.get('delta_alpha', global_config.get('delta_alpha', 0.2))
self._frame_alpha = config.get('frame_alpha', global_config.get('frame_alpha', 0.2))
self._frame_height = config.get('frame_height', global_config.get('frame_height', camera_height//6))
@property
def threshold(self):
return self._threshold
@property
def contour_area(self):
return self._contour_area
@property
def delta_alpha(self):
return self._delta_alpha
@property
def frame_alpha(self):
return self._frame_alpha
@property
def frame_height(self):
return self._frame_height
def to_dict(self):
return {
'threshold': self.threshold,
'contour_area': self.contour_area,
'delta_alpha': self.delta_alpha,
'frame_alpha': self.frame_alpha,
'frame_height': self.frame_height,
}
class DetectConfig():
def __init__(self, global_config, config, camera_fps):
self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
@property
def max_disappeared(self):
return self._max_disappeared
def to_dict(self):
return {
'max_disappeared': self._max_disappeared,
}
class ZoneConfig():
def __init__(self, name, config):
self._coordinates = config['coordinates']
@@ -584,40 +706,46 @@ class CameraConfig():
self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
self._snapshots = CameraSnapshotsConfig(config['snapshots'])
self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
self._motion = MotionConfig(global_config['motion'], config['motion'], self._height)
self._detect = DetectConfig(global_config['detect'], config['detect'], config.get('fps', 5))
self._ffmpeg_cmds = []
for ffmpeg_input in self._ffmpeg.inputs:
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
if ffmpeg_cmd is None:
continue
self._ffmpeg_cmds.append({
'roles': ffmpeg_input.roles,
'cmd': self._get_ffmpeg_cmd(ffmpeg_input)
'cmd': ffmpeg_cmd
})
self._set_zone_colors(self._zones)
def _create_mask(self, mask):
if mask:
if mask.startswith('base64,'):
img = base64.b64decode(mask[7:])
np_img = np.fromstring(img, dtype=np.uint8)
mask_img = cv2.imdecode(np_img, cv2.IMREAD_GRAYSCALE)
elif mask.startswith('poly,'):
points = mask.split(',')[1:]
contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
mask_img = np.zeros(self.frame_shape, np.uint8)
mask_img[:] = 255
cv2.fillPoly(mask_img, pts=[contour], color=(0))
else:
mask_img = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
else:
mask_img = None
mask_img = np.zeros(self.frame_shape, np.uint8)
mask_img[:] = 255
if mask_img is None or mask_img.size == 0:
mask_img = np.zeros(self.frame_shape, np.uint8)
mask_img[:] = 255
if isinstance(mask, list):
for m in mask:
self._add_mask(m, mask_img)
elif isinstance(mask, str):
self._add_mask(mask, mask_img)
return mask_img
def _add_mask(self, mask, mask_img):
if mask.startswith('poly,'):
points = mask.split(',')[1:]
contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
cv2.fillPoly(mask_img, pts=[contour], color=(0))
else:
mask_file = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
if not mask_file.size == 0:
mask_img[np.where(mask_file==[0])] = [0]
def _get_ffmpeg_cmd(self, ffmpeg_input):
ffmpeg_output_args = []
if 'detect' in ffmpeg_input.roles:
@@ -636,12 +764,19 @@ class CameraConfig():
ffmpeg_output_args = self.ffmpeg.output_args['record'] + [
f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
] + ffmpeg_output_args
return (['ffmpeg'] +
# if there arent any outputs enabled for this input
if len(ffmpeg_output_args) == 0:
return None
cmd = (['ffmpeg'] +
ffmpeg_input.global_args +
ffmpeg_input.hwaccel_args +
ffmpeg_input.input_args +
['-i', ffmpeg_input.path] +
ffmpeg_output_args)
return [part for part in cmd if part != '']
def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
# set colors for zones
@@ -705,6 +840,14 @@ class CameraConfig():
@property
def objects(self):
return self._objects
@property
def motion(self):
return self._motion
@property
def detect(self):
return self._detect
@property
def frame_shape(self):
@@ -731,6 +874,8 @@ class CameraConfig():
'rtmp': self.rtmp.to_dict(),
'snapshots': self.snapshots.to_dict(),
'objects': self.objects.to_dict(),
'motion': self.motion.to_dict(),
'detect': self.detect.to_dict(),
'frame_shape': self.frame_shape,
'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
}
@@ -747,6 +892,8 @@ class FrigateConfig():
config = self._sub_env_vars(config)
self._database = DatabaseConfig(config['database'])
self._model = ModelConfig(config['model'])
self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
self._mqtt = MqttConfig(config['mqtt'])
self._save_clips = SaveClipsConfig(config['save_clips'])
@@ -778,6 +925,8 @@ class FrigateConfig():
def to_dict(self):
return {
'database': self.database.to_dict(),
'model': self.model.to_dict(),
'detectors': {k: d.to_dict() for k, d in self.detectors.items()},
'mqtt': self.mqtt.to_dict(),
'save_clips': self.save_clips.to_dict(),
@@ -785,6 +934,14 @@ class FrigateConfig():
'logger': self.logger.to_dict()
}
@property
def database(self):
return self._database
@property
def model(self):
return self._model
@property
def detectors(self) -> Dict[str, DetectorConfig]:
return self._detectors

View File

@@ -43,7 +43,7 @@ class ObjectDetector(ABC):
pass
class LocalObjectDetector(ObjectDetector):
def __init__(self, tf_device=None, labels=None):
def __init__(self, tf_device=None, num_threads=3, labels=None):
self.fps = EventsPerSecond()
if labels is None:
self.labels = {}
@@ -66,7 +66,7 @@ class LocalObjectDetector(ObjectDetector):
if edge_tpu_delegate is None:
self.interpreter = tflite.Interpreter(
model_path='/cpu_model.tflite')
model_path='/cpu_model.tflite', num_threads=num_threads)
else:
self.interpreter = tflite.Interpreter(
model_path='/edgetpu_model.tflite',
@@ -106,7 +106,7 @@ class LocalObjectDetector(ObjectDetector):
return detections
def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, tf_device):
def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, model_shape, tf_device, num_threads):
threading.current_thread().name = f"detector:{name}"
logger = logging.getLogger(f"detector.{name}")
logger.info(f"Starting detection process: {os.getpid()}")
@@ -120,7 +120,7 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
signal.signal(signal.SIGINT, receiveSignal)
frame_manager = SharedMemoryFrameManager()
object_detector = LocalObjectDetector(tf_device=tf_device)
object_detector = LocalObjectDetector(tf_device=tf_device, num_threads=num_threads)
outputs = {}
for name in out_events.keys():
@@ -139,7 +139,7 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
connection_id = detection_queue.get(timeout=5)
except queue.Empty:
continue
input_frame = frame_manager.get(connection_id, (1,300,300,3))
input_frame = frame_manager.get(connection_id, (1,model_shape[0],model_shape[1],3))
if input_frame is None:
continue
@@ -155,14 +155,16 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
avg_speed.value = (avg_speed.value*9 + duration)/10
class EdgeTPUProcess():
def __init__(self, name, detection_queue, out_events, tf_device=None):
def __init__(self, name, detection_queue, out_events, model_shape, tf_device=None, num_threads=3):
self.name = name
self.out_events = out_events
self.detection_queue = detection_queue
self.avg_inference_speed = mp.Value('d', 0.01)
self.detection_start = mp.Value('d', 0.0)
self.detect_process = None
self.model_shape = model_shape
self.tf_device = tf_device
self.num_threads = num_threads
self.start_or_restart()
def stop(self):
@@ -178,19 +180,19 @@ class EdgeTPUProcess():
self.detection_start.value = 0.0
if (not self.detect_process is None) and self.detect_process.is_alive():
self.stop()
self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.tf_device))
self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.model_shape, self.tf_device, self.num_threads))
self.detect_process.daemon = True
self.detect_process.start()
class RemoteObjectDetector():
def __init__(self, name, labels, detection_queue, event):
def __init__(self, name, labels, detection_queue, event, model_shape):
self.labels = load_labels(labels)
self.name = name
self.fps = EventsPerSecond()
self.detection_queue = detection_queue
self.event = event
self.shm = mp.shared_memory.SharedMemory(name=self.name, create=False)
self.np_shm = np.ndarray((1,300,300,3), dtype=np.uint8, buffer=self.shm.buf)
self.np_shm = np.ndarray((1,model_shape[0],model_shape[1],3), dtype=np.uint8, buffer=self.shm.buf)
self.out_shm = mp.shared_memory.SharedMemory(name=f"out-{self.name}", create=False)
self.out_np_shm = np.ndarray((20,6), dtype=np.float32, buffer=self.out_shm.buf)

View File

@@ -36,9 +36,10 @@ class EventProcessor(threading.Thread):
files_in_use = []
for process in psutil.process_iter():
if process.name() != 'ffmpeg':
continue
try:
if process.name() != 'ffmpeg':
continue
flist = process.open_files()
if flist:
for nt in flist:
@@ -96,18 +97,18 @@ class EventProcessor(threading.Thread):
del self.cached_clips[f]
os.remove(os.path.join(CACHE_DIR,f))
def create_clip(self, camera, event_data, pre_capture):
def create_clip(self, camera, event_data, pre_capture, post_capture):
# get all clips from the camera with the event sorted
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
while sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']:
while len(sorted_clips) == 0 or sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']+post_capture:
time.sleep(5)
self.refresh_cache()
# get all clips from the camera with the event sorted
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
playlist_start = event_data['start_time']-pre_capture
playlist_end = event_data['end_time']+5
playlist_end = event_data['end_time']+post_capture
playlist_lines = []
for clip in sorted_clips:
# clip ends before playlist start time, skip
@@ -138,6 +139,8 @@ class EventProcessor(threading.Thread):
'-',
'-c',
'copy',
'-movflags',
'+faststart',
f"{os.path.join(CLIPS_DIR, clip_name)}.mp4"
]
@@ -180,7 +183,7 @@ class EventProcessor(threading.Thread):
if event_type == 'end':
if len(self.cached_clips) > 0 and not event_data['false_positive']:
self.create_clip(camera, event_data, save_clips_config.pre_capture)
self.create_clip(camera, event_data, save_clips_config.pre_capture, save_clips_config.post_capture)
Event.create(
id=event_data['id'],
label=event_data['label'],

View File

@@ -13,6 +13,8 @@ from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict
from frigate.models import Event
from frigate.util import calculate_region
from frigate.version import VERSION
logger = logging.getLogger(__name__)
@@ -144,6 +146,10 @@ def events():
def config():
return jsonify(current_app.frigate_config.to_dict())
@bp.route('/version')
def version():
return VERSION
@bp.route('/stats')
def stats():
camera_metrics = current_app.camera_metrics
@@ -185,7 +191,8 @@ def best(camera_name, label):
crop = bool(request.args.get('crop', 0, type=int))
if crop:
region = best_object.get('region', [0,0,300,300])
box = best_object.get('box', (0,0,300,300))
region = calculate_region(best_frame.shape, box[0], box[1], box[2], box[3], 1.1)
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
height = int(request.args.get('h', str(best_frame.shape[0])))
@@ -203,18 +210,34 @@ def best(camera_name, label):
def mjpeg_feed(camera_name):
fps = int(request.args.get('fps', '3'))
height = int(request.args.get('h', '360'))
draw_options = {
'bounding_boxes': request.args.get('bbox', type=int),
'timestamp': request.args.get('timestamp', type=int),
'zones': request.args.get('zones', type=int),
'mask': request.args.get('mask', type=int),
'motion_boxes': request.args.get('motion', type=int),
'regions': request.args.get('regions', type=int),
}
if camera_name in current_app.frigate_config.cameras:
# return a multipart response
return Response(imagestream(current_app.detected_frames_processor, camera_name, fps, height),
return Response(imagestream(current_app.detected_frames_processor, camera_name, fps, height, draw_options),
mimetype='multipart/x-mixed-replace; boundary=frame')
else:
return "Camera named {} not found".format(camera_name), 404
@bp.route('/<camera_name>/latest.jpg')
def latest_frame(camera_name):
draw_options = {
'bounding_boxes': request.args.get('bbox', type=int),
'timestamp': request.args.get('timestamp', type=int),
'zones': request.args.get('zones', type=int),
'mask': request.args.get('mask', type=int),
'motion_boxes': request.args.get('motion', type=int),
'regions': request.args.get('regions', type=int),
}
if camera_name in current_app.frigate_config.cameras:
# max out at specified FPS
frame = current_app.detected_frames_processor.get_current_frame(camera_name)
frame = current_app.detected_frames_processor.get_current_frame(camera_name, draw_options)
if frame is None:
frame = np.zeros((720,1280,3), np.uint8)
@@ -230,11 +253,11 @@ def latest_frame(camera_name):
else:
return "Camera named {} not found".format(camera_name), 404
def imagestream(detected_frames_processor, camera_name, fps, height):
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
while True:
# max out at specified FPS
time.sleep(1/fps)
frame = detected_frames_processor.get_current_frame(camera_name, draw=True)
frame = detected_frames_processor.get_current_frame(camera_name, draw_options)
if frame is None:
frame = np.zeros((height,int(height*16/9),3), np.uint8)

View File

@@ -1,13 +1,15 @@
import cv2
import imutils
import numpy as np
from frigate.config import MotionConfig
class MotionDetector():
def __init__(self, frame_shape, mask, resize_factor=4):
def __init__(self, frame_shape, mask, config: MotionConfig):
self.config = config
self.frame_shape = frame_shape
self.resize_factor = resize_factor
self.motion_frame_size = (int(frame_shape[0]/resize_factor), int(frame_shape[1]/resize_factor))
self.resize_factor = frame_shape[0]/config.frame_height
self.motion_frame_size = (config.frame_height, config.frame_height*frame_shape[1]//frame_shape[0])
self.avg_frame = np.zeros(self.motion_frame_size, np.float)
self.avg_delta = np.zeros(self.motion_frame_size, np.float)
self.motion_frame_count = 0
@@ -23,6 +25,8 @@ class MotionDetector():
# resize frame
resized_frame = cv2.resize(gray, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
# TODO: can I improve the contrast of the grayscale image here?
# convert to grayscale
# resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY)
@@ -38,14 +42,13 @@ class MotionDetector():
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
# compute the average delta over the past few frames
# the alpha value can be modified to configure how sensitive the motion detection is.
# higher values mean the current frame impacts the delta a lot, and a single raindrop may
# register as motion, too low and a fast moving person wont be detected as motion
# this also assumes that a person is in the same location across more than a single frame
cv2.accumulateWeighted(frameDelta, self.avg_delta, 0.2)
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
# compute the threshold image for the current frame
current_thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1]
# TODO: threshold
current_thresh = cv2.threshold(frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
# black out everything in the avg_delta where there isnt motion in the current frame
avg_delta_image = cv2.convertScaleAbs(self.avg_delta)
@@ -53,7 +56,7 @@ class MotionDetector():
# then look for deltas above the threshold, but only in areas where there is a delta
# in the current frame. this prevents deltas from previous frames from being included
thresh = cv2.threshold(avg_delta_image, 25, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.threshold(avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
# dilate the thresholded image to fill in holes, then find contours
# on thresholded image
@@ -65,19 +68,18 @@ class MotionDetector():
for c in cnts:
# if the contour is big enough, count it as motion
contour_area = cv2.contourArea(c)
if contour_area > 100:
if contour_area > self.config.contour_area:
x, y, w, h = cv2.boundingRect(c)
motion_boxes.append((x*self.resize_factor, y*self.resize_factor, (x+w)*self.resize_factor, (y+h)*self.resize_factor))
motion_boxes.append((int(x*self.resize_factor), int(y*self.resize_factor), int((x+w)*self.resize_factor), int((y+h)*self.resize_factor)))
if len(motion_boxes) > 0:
self.motion_frame_count += 1
# TODO: this really depends on FPS
if self.motion_frame_count >= 10:
# only average in the current frame if the difference persists for at least 3 frames
cv2.accumulateWeighted(resized_frame, self.avg_frame, 0.2)
# only average in the current frame if the difference persists for a bit
cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
else:
# when no motion, just keep averaging the frames together
cv2.accumulateWeighted(resized_frame, self.avg_frame, 0.2)
cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
self.motion_frame_count = 0
return motion_boxes

View File

@@ -20,7 +20,7 @@ import numpy as np
from frigate.config import FrigateConfig, CameraConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
from frigate.edgetpu import load_labels
from frigate.util import SharedMemoryFrameManager, draw_box_with_label
from frigate.util import SharedMemoryFrameManager, draw_box_with_label, calculate_region
logger = logging.getLogger(__name__)
@@ -73,7 +73,7 @@ class TrackedObject():
self.top_score = self.computed_score = 0.0
self.thumbnail_data = None
self.frame = None
self.previous = None
self.previous = self.to_dict()
self._snapshot_jpg_time = 0
ret, jpg = cv2.imencode('.jpg', np.zeros((300,300,3), np.uint8))
self._snapshot_jpg = jpg.tobytes()
@@ -99,7 +99,7 @@ class TrackedObject():
return median(scores)
def update(self, current_frame_time, obj_data):
previous = self.to_dict()
significant_update = False
self.obj_data.update(obj_data)
# if the object is not in the current frame, add a 0.0 to the score history
if self.obj_data['frame_time'] != current_frame_time:
@@ -129,7 +129,7 @@ class TrackedObject():
'region': self.obj_data['region'],
'score': self.obj_data['score']
}
self.previous = previous
significant_update = True
# check zones
current_zones = []
@@ -143,8 +143,13 @@ class TrackedObject():
if name in self.current_zones or not zone_filtered(self, zone.filters):
current_zones.append(name)
self.entered_zones.add(name)
# if the zones changed, signal an update
if not self.false_positive and set(self.current_zones) != set(current_zones):
significant_update = True
self.current_zones = current_zones
return significant_update
def to_dict(self, include_thumbnail: bool = False):
return {
@@ -187,7 +192,8 @@ class TrackedObject():
f"{int(self.thumbnail_data['score']*100)}% {int(self.thumbnail_data['area'])}", thickness=thickness, color=color)
if snapshot_config.crop_to_region:
region = self.thumbnail_data['region']
box = self.thumbnail_data['box']
region = calculate_region(best_frame.shape, box[0], box[1], box[2], box[3], 1.1)
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
if snapshot_config.height:
@@ -250,15 +256,17 @@ class CameraState():
self.previous_frame_id = None
self.callbacks = defaultdict(lambda: [])
def get_current_frame(self, draw=False):
def get_current_frame(self, draw_options={}):
with self.current_frame_lock:
frame_copy = np.copy(self._current_frame)
frame_time = self.current_frame_time
tracked_objects = {k: v.to_dict() for k,v in self.tracked_objects.items()}
motion_boxes = self.motion_boxes.copy()
regions = self.regions.copy()
frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420)
# draw on the frame
if draw:
if draw_options.get('bounding_boxes'):
# draw the bounding boxes on the frame
for obj in tracked_objects.values():
thickness = 2
@@ -271,19 +279,28 @@ class CameraState():
# draw the bounding boxes on the frame
box = obj['box']
draw_box_with_label(frame_copy, 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(frame_copy, (region[0], region[1]), (region[2], region[3]), (0,255,0), 1)
if self.camera_config.snapshots.show_timestamp:
time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S")
cv2.putText(frame_copy, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
if self.camera_config.snapshots.draw_zones:
for name, zone in self.camera_config.zones.items():
thickness = 8 if any([name in obj['current_zones'] for obj in tracked_objects.values()]) else 2
cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness)
if draw_options.get('regions'):
for region in regions:
cv2.rectangle(frame_copy, (region[0], region[1]), (region[2], region[3]), (0,255,0), 2)
if draw_options.get('timestamp'):
time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S")
cv2.putText(frame_copy, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
if draw_options.get('zones'):
for name, zone in self.camera_config.zones.items():
thickness = 8 if any([name in obj['current_zones'] for obj in tracked_objects.values()]) else 2
cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness)
if draw_options.get('mask'):
mask_overlay = np.where(self.camera_config.mask==[0])
frame_copy[mask_overlay] = [0,0,0]
if draw_options.get('motion_boxes'):
for m_box in motion_boxes:
cv2.rectangle(frame_copy, (m_box[0], m_box[1]), (m_box[2], m_box[3]), (0,0,255), 2)
return frame_copy
def finished(self, obj_id):
@@ -292,8 +309,10 @@ class CameraState():
def on(self, event_type: str, callback: Callable[[Dict], None]):
self.callbacks[event_type].append(callback)
def update(self, frame_time, current_detections):
def update(self, frame_time, current_detections, motion_boxes, regions):
self.current_frame_time = frame_time
self.motion_boxes = motion_boxes
self.regions = regions
# get the new frame
frame_id = f"{self.name}{frame_time}"
current_frame = self.frame_manager.get(frame_id, self.camera_config.frame_shape_yuv)
@@ -313,16 +332,16 @@ class CameraState():
for id in updated_ids:
updated_obj = self.tracked_objects[id]
updated_obj.update(frame_time, current_detections[id])
significant_update = updated_obj.update(frame_time, current_detections[id])
if (not updated_obj.false_positive
and updated_obj.thumbnail_data['frame_time'] == frame_time
and frame_time not in self.frame_cache):
self.frame_cache[frame_time] = np.copy(current_frame)
if significant_update:
# ensure this frame is stored in the cache
if updated_obj.thumbnail_data['frame_time'] == frame_time and frame_time not in self.frame_cache:
self.frame_cache[frame_time] = np.copy(current_frame)
# call event handlers
for c in self.callbacks['update']:
c(self.name, updated_obj, frame_time)
# call event handlers
for c in self.callbacks['update']:
c(self.name, updated_obj, frame_time)
for id in removed_ids:
# publish events to mqtt
@@ -407,9 +426,10 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue.put(('start', camera, obj.to_dict()))
def update(camera, obj: TrackedObject, current_frame_time):
if not obj.thumbnail_data is None and obj.thumbnail_data['frame_time'] == current_frame_time:
message = { 'before': obj.previous, 'after': obj.to_dict() }
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
after = obj.to_dict()
message = { 'before': obj.previous, 'after': after }
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
obj.previous = after
def end(camera, obj: TrackedObject, current_frame_time):
if not obj.false_positive:
@@ -447,14 +467,14 @@ class TrackedObjectProcessor(threading.Thread):
camera_state = self.camera_states[camera]
if label in camera_state.best_objects:
best_obj = camera_state.best_objects[label]
best = best_obj.to_dict()
best['frame'] = camera_state.frame_cache[best_obj.thumbnail_data['frame_time']]
best = best_obj.thumbnail_data.copy()
best['frame'] = camera_state.frame_cache.get(best_obj.thumbnail_data['frame_time'])
return best
else:
return {}
def get_current_frame(self, camera, draw=False):
return self.camera_states[camera].get_current_frame(draw)
def get_current_frame(self, camera, draw_options={}):
return self.camera_states[camera].get_current_frame(draw_options)
def run(self):
while True:
@@ -463,13 +483,13 @@ class TrackedObjectProcessor(threading.Thread):
break
try:
camera, frame_time, current_tracked_objects = self.tracked_objects_queue.get(True, 10)
camera, frame_time, current_tracked_objects, motion_boxes, regions = self.tracked_objects_queue.get(True, 10)
except queue.Empty:
continue
camera_state = self.camera_states[camera]
camera_state.update(frame_time, current_tracked_objects)
camera_state.update(frame_time, current_tracked_objects, motion_boxes, regions)
# update zone counts for each label
# for each zone in the current camera

View File

@@ -12,14 +12,15 @@ import cv2
import numpy as np
from scipy.spatial import distance as dist
from frigate.util import calculate_region, draw_box_with_label
from frigate.config import DetectConfig
from frigate.util import draw_box_with_label
class ObjectTracker():
def __init__(self, max_disappeared):
def __init__(self, config: DetectConfig):
self.tracked_objects = {}
self.disappeared = {}
self.max_disappeared = max_disappeared
self.max_disappeared = config.max_disappeared
def register(self, index, obj):
rand_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))

208
frigate/process_clip.py Normal file
View File

@@ -0,0 +1,208 @@
import datetime
import json
import logging
import multiprocessing as mp
import os
import subprocess as sp
import sys
from unittest import TestCase, main
import click
import cv2
import numpy as np
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig
from frigate.edgetpu import LocalObjectDetector
from frigate.motion import MotionDetector
from frigate.object_processing import COLOR_MAP, CameraState
from frigate.objects import ObjectTracker
from frigate.util import (DictFrameManager, EventsPerSecond,
SharedMemoryFrameManager, draw_box_with_label)
from frigate.video import (capture_frames, process_frames,
start_or_restart_ffmpeg)
logging.basicConfig()
logging.root.setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
def get_frame_shape(source):
ffprobe_cmd = " ".join([
'ffprobe',
'-v',
'panic',
'-show_error',
'-show_streams',
'-of',
'json',
'"'+source+'"'
])
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
(output, err) = p.communicate()
p_status = p.wait()
info = json.loads(output)
video_info = [s for s in info['streams'] if s['codec_type'] == 'video'][0]
if video_info['height'] != 0 and video_info['width'] != 0:
return (video_info['height'], video_info['width'], 3)
# fallback to using opencv if ffprobe didnt succeed
video = cv2.VideoCapture(source)
ret, frame = video.read()
frame_shape = frame.shape
video.release()
return frame_shape
class ProcessClip():
def __init__(self, clip_path, frame_shape, config: FrigateConfig):
self.clip_path = clip_path
self.camera_name = 'camera'
self.config = config
self.camera_config = self.config.cameras['camera']
self.frame_shape = self.camera_config.frame_shape
self.ffmpeg_cmd = [c['cmd'] for c in self.camera_config.ffmpeg_cmds if 'detect' in c['roles']][0]
self.frame_manager = SharedMemoryFrameManager()
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()
current_frame = mp.Value('d', 0.0)
frame_size = self.camera_config.frame_shape_yuv[0] * self.camera_config.frame_shape_yuv[1]
ffmpeg_process = start_or_restart_ffmpeg(self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size)
capture_frames(ffmpeg_process, self.camera_name, self.camera_config.frame_shape_yuv, self.frame_manager,
self.frame_queue, fps, skipped_fps, 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, self.camera_config.motion)
object_detector = LocalObjectDetector(labels='/labelmap.txt')
object_tracker = ObjectTracker(self.camera_config.detect)
process_info = {
'process_fps': mp.Value('d', 0.0),
'detection_fps': mp.Value('d', 0.0),
'detection_frame': mp.Value('d', 0.0)
}
stop_event = mp.Event()
model_shape = (self.config.model.height, self.config.model.width)
process_frames(self.camera_name, self.frame_queue, self.frame_shape, model_shape,
self.frame_manager, motion_detector, object_detector, object_tracker,
self.detected_objects_queue, process_info,
objects_to_track, object_filters, mask, stop_event, exit_on_empty=True)
def top_object(self, debug_path=None):
obj_detected = False
top_computed_score = 0.0
def handle_event(name, obj, frame_time):
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, motion_boxes, regions = 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, motion_boxes, regions)
self.frame_manager.delete(self.camera_state.previous_frame_id)
return {
'object_detected': obj_detected,
'top_score': top_computed_score
}
def save_debug_frame(self, debug_path, frame_time, tracked_objects):
current_frame = cv2.cvtColor(self.frame_manager.get(f"{self.camera_name}{frame_time}", self.camera_config.frame_shape_yuv), cv2.COLOR_YUV2BGR_I420)
# 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['id'], 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", current_frame)
@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("-s", "--scores", default=None, help="File to save csv of top scores")
@click.option("--debug-path", default=None, help="Path to output frames for debugging.")
def process(path, label, threshold, scores, 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)
json_config = {
'mqtt': {
'host': 'mqtt'
},
'cameras': {
'camera': {
'ffmpeg': {
'inputs': [
{ 'path': 'path.mp4', 'global_args': '', 'input_args': '', 'roles': ['detect'] }
]
},
'height': 1920,
'width': 1080
}
}
}
results = []
for c in clips:
logger.info(c)
frame_shape = get_frame_shape(c)
json_config['cameras']['camera']['height'] = frame_shape[0]
json_config['cameras']['camera']['width'] = frame_shape[1]
json_config['cameras']['camera']['ffmpeg']['inputs'][0]['path'] = c
config = FrigateConfig(config=FRIGATE_CONFIG_SCHEMA(json_config))
process_clip = ProcessClip(c, frame_shape, config)
process_clip.load_frames()
process_clip.process_frames(objects_to_track=[label])
results.append((c, process_clip.top_object(debug_path)))
if not scores is None:
with open(scores, 'w') as writer:
for result in results:
writer.write(f"{result[0]},{result[1]['top_score']}\n")
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()

View File

@@ -314,6 +314,31 @@ class TestConfig(TestCase):
assert(len(config.cameras['back'].save_clips.objects) == 2)
assert('dog' in config.cameras['back'].save_clips.objects)
assert('person' in config.cameras['back'].save_clips.objects)
def test_role_assigned_but_not_enabled(self):
json_config = {
'mqtt': {
'host': 'mqtt'
},
'cameras': {
'back': {
'ffmpeg': {
'inputs': [
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect', 'rtmp'] },
{ 'path': 'rtsp://10.0.0.1:554/clips', 'roles': ['clips'] }
]
},
'height': 1080,
'width': 1920
}
}
}
config = FrigateConfig(config=json_config)
ffmpeg_cmds = config.cameras['back'].ffmpeg_cmds
assert(len(ffmpeg_cmds) == 1)
assert(not 'clips' in ffmpeg_cmds[0]['roles'])
if __name__ == '__main__':
main(verbosity=2)

View File

@@ -0,0 +1,39 @@
import cv2
import numpy as np
from unittest import TestCase, main
from frigate.util import yuv_region_2_rgb
class TestYuvRegion2RGB(TestCase):
def setUp(self):
self.bgr_frame = np.zeros((100, 200, 3), np.uint8)
self.bgr_frame[:] = (0, 0, 255)
self.bgr_frame[5:55, 5:55] = (255,0,0)
# cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame)
self.yuv_frame = cv2.cvtColor(self.bgr_frame, cv2.COLOR_BGR2YUV_I420)
def test_crop_yuv(self):
cropped = yuv_region_2_rgb(self.yuv_frame, (10,10,50,50))
# ensure the upper left pixel is blue
assert(np.all(cropped[0, 0] == [0, 0, 255]))
def test_crop_yuv_out_of_bounds(self):
cropped = yuv_region_2_rgb(self.yuv_frame, (0,0,200,200))
# cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
# ensure the upper left pixel is red
# the yuv conversion has some noise
assert(np.all(cropped[0, 0] == [255, 1, 0]))
# ensure the bottom right is black
assert(np.all(cropped[199, 199] == [0, 0, 0]))
def test_crop_yuv_portrait(self):
bgr_frame = np.zeros((1920, 1080, 3), np.uint8)
bgr_frame[:] = (0, 0, 255)
bgr_frame[5:55, 5:55] = (255,0,0)
# cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame)
yuv_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2YUV_I420)
cropped = yuv_region_2_rgb(yuv_frame, (0, 852, 648, 1500))
# cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
if __name__ == '__main__':
main(verbosity=2)

View File

@@ -47,14 +47,11 @@ def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thicknes
cv2.putText(frame, display_text, (text_offset_x, text_offset_y + line_height - 3), font, fontScale=font_scale, color=(0, 0, 0), thickness=2)
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)
# size is the longest edge and divisible by 4
size = int(max(xmax-xmin, ymax-ymin)//4*4*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])
# x_offset is midpoint of bounding box minus half the size
x_offset = int((xmax-xmin)/2.0+xmin-size/2.0)
@@ -62,48 +59,156 @@ def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
if x_offset < 0:
x_offset = 0
elif x_offset > (frame_shape[1]-size):
x_offset = (frame_shape[1]-size)
x_offset = max(0, (frame_shape[1]-size))
# y_offset is midpoint of bounding box minus half the size
y_offset = int((ymax-ymin)/2.0+ymin-size/2.0)
# if outside the image
# # if outside the image
if y_offset < 0:
y_offset = 0
elif y_offset > (frame_shape[0]-size):
y_offset = (frame_shape[0]-size)
y_offset = max(0, (frame_shape[0]-size))
return (x_offset, y_offset, x_offset+size, y_offset+size)
def get_yuv_crop(frame_shape, crop):
# crop should be (x1,y1,x2,y2)
frame_height = frame_shape[0]//3*2
frame_width = frame_shape[1]
# compute the width/height of the uv channels
uv_width = frame_width//2 # width of the uv channels
uv_height = frame_height//4 # height of the uv channels
# compute the offset for upper left corner of the uv channels
uv_x_offset = crop[0]//2 # x offset of the uv channels
uv_y_offset = crop[1]//4 # y offset of the uv channels
# compute the width/height of the uv crops
uv_crop_width = (crop[2] - crop[0])//2 # width of the cropped uv channels
uv_crop_height = (crop[3] - crop[1])//4 # height of the cropped uv channels
# ensure crop dimensions are multiples of 2 and 4
y = (
crop[0],
crop[1],
crop[0] + uv_crop_width*2,
crop[1] + uv_crop_height*4
)
u1 = (
0 + uv_x_offset,
frame_height + uv_y_offset,
0 + uv_x_offset + uv_crop_width,
frame_height + uv_y_offset + uv_crop_height
)
u2 = (
uv_width + uv_x_offset,
frame_height + uv_y_offset,
uv_width + uv_x_offset + uv_crop_width,
frame_height + uv_y_offset + uv_crop_height
)
v1 = (
0 + uv_x_offset,
frame_height + uv_height + uv_y_offset,
0 + uv_x_offset + uv_crop_width,
frame_height + uv_height + uv_y_offset + uv_crop_height
)
v2 = (
uv_width + uv_x_offset,
frame_height + uv_height + uv_y_offset,
uv_width + uv_x_offset + uv_crop_width,
frame_height + uv_height + uv_y_offset + uv_crop_height
)
return y, u1, u2, v1, v2
def yuv_region_2_rgb(frame, region):
height = frame.shape[0]//3*2
width = frame.shape[1]
# make sure the size is a multiple of 4
size = (region[3] - region[1])//4*4
try:
height = frame.shape[0]//3*2
width = frame.shape[1]
x1 = region[0]
y1 = region[1]
# get the crop box if the region extends beyond the frame
crop_x1 = max(0, region[0])
crop_y1 = max(0, region[1])
# ensure these are a multiple of 4
crop_x2 = min(width, region[2])
crop_y2 = min(height, region[3])
crop_box = (crop_x1, crop_y1, crop_x2, crop_y2)
uv_x1 = x1//2
uv_y1 = y1//4
y, u1, u2, v1, v2 = get_yuv_crop(frame.shape, crop_box)
uv_width = size//2
uv_height = size//4
# if the region starts outside the frame, indent the start point in the cropped frame
y_channel_x_offset = abs(min(0, region[0]))
y_channel_y_offset = abs(min(0, region[1]))
u_y_start = height
v_y_start = height + height//4
two_x_offset = width//2
uv_channel_x_offset = y_channel_x_offset//2
uv_channel_y_offset = y_channel_y_offset//4
yuv_cropped_frame = np.zeros((size+size//2, size), np.uint8)
# y channel
yuv_cropped_frame[0:size, 0:size] = frame[y1:y1+size, x1:x1+size]
# u channel
yuv_cropped_frame[size:size+uv_height, 0:uv_width] = frame[uv_y1+u_y_start:uv_y1+u_y_start+uv_height, uv_x1:uv_x1+uv_width]
yuv_cropped_frame[size:size+uv_height, uv_width:size] = frame[uv_y1+u_y_start:uv_y1+u_y_start+uv_height, uv_x1+two_x_offset:uv_x1+two_x_offset+uv_width]
# v channel
yuv_cropped_frame[size+uv_height:size+uv_height*2, 0:uv_width] = frame[uv_y1+v_y_start:uv_y1+v_y_start+uv_height, uv_x1:uv_x1+uv_width]
yuv_cropped_frame[size+uv_height:size+uv_height*2, uv_width:size] = frame[uv_y1+v_y_start:uv_y1+v_y_start+uv_height, uv_x1+two_x_offset:uv_x1+two_x_offset+uv_width]
# create the yuv region frame
# make sure the size is a multiple of 4
size = (region[3] - region[1])//4*4
yuv_cropped_frame = np.zeros((size+size//2, size), np.uint8)
# fill in black
yuv_cropped_frame[:] = 128
yuv_cropped_frame[0:size,0:size] = 16
return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420)
# copy the y channel
yuv_cropped_frame[
y_channel_y_offset:y_channel_y_offset + y[3] - y[1],
y_channel_x_offset:y_channel_x_offset + y[2] - y[0]
] = frame[
y[1]:y[3],
y[0]:y[2]
]
uv_crop_width = u1[2] - u1[0]
uv_crop_height = u1[3] - u1[1]
# copy u1
yuv_cropped_frame[
size + uv_channel_y_offset:size + uv_channel_y_offset + uv_crop_height,
0 + uv_channel_x_offset:0 + uv_channel_x_offset + uv_crop_width
] = frame[
u1[1]:u1[3],
u1[0]:u1[2]
]
# copy u2
yuv_cropped_frame[
size + uv_channel_y_offset:size + uv_channel_y_offset + uv_crop_height,
size//2 + uv_channel_x_offset:size//2 + uv_channel_x_offset + uv_crop_width
] = frame[
u2[1]:u2[3],
u2[0]:u2[2]
]
# copy v1
yuv_cropped_frame[
size+size//4 + uv_channel_y_offset:size+size//4 + uv_channel_y_offset + uv_crop_height,
0 + uv_channel_x_offset:0 + uv_channel_x_offset + uv_crop_width
] = frame[
v1[1]:v1[3],
v1[0]:v1[2]
]
# copy v2
yuv_cropped_frame[
size+size//4 + uv_channel_y_offset:size+size//4 + uv_channel_y_offset + uv_crop_height,
size//2 + uv_channel_x_offset:size//2 + uv_channel_x_offset + uv_crop_width
] = frame[
v2[1]:v2[3],
v2[0]:v2[2]
]
return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420)
except:
print(f"frame.shape: {frame.shape}")
print(f"region: {region}")
raise
def intersection(box_a, box_b):
return (

View File

@@ -64,14 +64,14 @@ def filtered(obj, objects_to_track, object_filters, mask=None):
return False
def create_tensor_input(frame, region):
def create_tensor_input(frame, model_shape, region):
cropped_frame = yuv_region_2_rgb(frame, region)
# Resize to 300x300 if needed
if cropped_frame.shape != (300, 300, 3):
cropped_frame = cv2.resize(cropped_frame, dsize=(300, 300), interpolation=cv2.INTER_LINEAR)
if cropped_frame.shape != (model_shape[0], model_shape[1], 3):
cropped_frame = cv2.resize(cropped_frame, dsize=model_shape, interpolation=cv2.INTER_LINEAR)
# Expand dimensions since the model expects images to have shape: [1, 300, 300, 3]
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
return np.expand_dims(cropped_frame, axis=0)
def stop_ffmpeg(ffmpeg_process, logger):
@@ -112,16 +112,15 @@ def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: Fram
frame_name = f"{camera_name}{current_frame.value}"
frame_buffer = frame_manager.create(frame_name, frame_size)
try:
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
except:
logger.info(f"{camera_name}: ffmpeg sent a broken frame. something is wrong.")
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
except Exception as e:
logger.info(f"{camera_name}: ffmpeg sent a broken frame. {e}")
if ffmpeg_process.poll() != None:
logger.info(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
frame_manager.delete(frame_name)
break
continue
if ffmpeg_process.poll() != None:
logger.info(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
frame_manager.delete(frame_name)
break
continue
frame_rate.update()
@@ -241,7 +240,7 @@ def capture_camera(name, config: CameraConfig, process_info):
camera_watchdog.start()
camera_watchdog.join()
def track_camera(name, config: CameraConfig, detection_queue, result_connection, detected_objects_queue, process_info):
def track_camera(name, config: CameraConfig, model_shape, detection_queue, result_connection, detected_objects_queue, process_info):
stop_event = mp.Event()
def receiveSignal(signalNumber, frame):
stop_event.set()
@@ -259,14 +258,14 @@ def track_camera(name, config: CameraConfig, detection_queue, result_connection,
object_filters = config.objects.filters
mask = config.mask
motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection)
motion_detector = MotionDetector(frame_shape, mask, config.motion)
object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection, model_shape)
object_tracker = ObjectTracker(10)
object_tracker = ObjectTracker(config.detect)
frame_manager = SharedMemoryFrameManager()
process_frames(name, frame_queue, frame_shape, frame_manager, motion_detector, object_detector,
process_frames(name, frame_queue, frame_shape, model_shape, frame_manager, motion_detector, object_detector,
object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, mask, stop_event)
logger.info(f"{name}: exiting subprocess")
@@ -277,8 +276,8 @@ def reduce_boxes(boxes):
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)
def detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask):
tensor_input = create_tensor_input(frame, model_shape, region)
detections = []
region_detections = object_detector.detect(tensor_input)
@@ -300,7 +299,7 @@ def detect(object_detector, frame, region, objects_to_track, object_filters, mas
detections.append(det)
return detections
def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, model_shape,
frame_manager: FrameManager, motion_detector: MotionDetector,
object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
detected_objects_queue: mp.Queue, process_info: Dict,
@@ -357,7 +356,7 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
# resize regions and detect
detections = []
for region in regions:
detections.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
detections.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask))
#########
# merge objects, check for clipped objects and look again up to 4 times
@@ -389,8 +388,10 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
region = calculate_region(frame_shape,
box[0], box[1],
box[2], box[3])
regions.append(region)
selected_objects.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
selected_objects.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask))
refining = True
else:
@@ -412,6 +413,6 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
else:
fps_tracker.update()
fps.value = fps_tracker.eps()
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects))
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects, motion_boxes, regions))
detection_fps.value = object_detector.fps.eps()
frame_manager.close(f"{camera_name}{frame_time}")

View File

@@ -1,152 +0,0 @@
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, SharedMemoryFrameManager, 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_manager = SharedMemoryFrameManager()
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']}")
self.frame_manager.delete(self.camera_state.previous_frame_id)
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}", self.frame_shape)
# 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)
config['frame_shape'] = frame_shape
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()