Compare commits
121 Commits
v0.8.0-bet
...
v0.8.0-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0232a9f94a | ||
|
|
e8586d6459 | ||
|
|
9cf49b5bc4 | ||
|
|
4a18fc58de | ||
|
|
5ddfde2e72 | ||
|
|
714d76887d | ||
|
|
0c0e1416ff | ||
|
|
c6044ba9a1 | ||
|
|
a7739a0a62 | ||
|
|
84ed126db6 | ||
|
|
a76f54c326 | ||
|
|
b93d354c60 | ||
|
|
14d218af46 | ||
|
|
bd4973e3f7 | ||
|
|
d94f81969b | ||
|
|
d32fed2c01 | ||
|
|
7b4e510b95 | ||
|
|
bb4f79cdfe | ||
|
|
e32e69c2d0 | ||
|
|
a71ae053e4 | ||
|
|
fcc9cd56cc | ||
|
|
b981a3110b | ||
|
|
2da50cc538 | ||
|
|
cb4a0aa594 | ||
|
|
52da1fddc7 | ||
|
|
14645ce4f8 | ||
|
|
97ce7f3028 | ||
|
|
3b5302f6ea | ||
|
|
74eb16f213 | ||
|
|
a3d6bf214c | ||
|
|
16121ffd00 | ||
|
|
91628bd5d8 | ||
|
|
b10b64bf57 | ||
|
|
749c34be9f | ||
|
|
8cfdfab985 | ||
|
|
ef25f8a31e | ||
|
|
2a0551a08a | ||
|
|
0b80419f15 | ||
|
|
0dc81117aa | ||
|
|
49b29d72a7 | ||
|
|
21ece238ff | ||
|
|
f6ba3f2daa | ||
|
|
bb0d3cb59a | ||
|
|
ca9b6d6c5c | ||
|
|
3103ad2bfe | ||
|
|
eab3998ad0 | ||
|
|
a3dfd3a8e0 | ||
|
|
f1c3087775 | ||
|
|
1be91ed3f2 | ||
|
|
fd83c4f229 | ||
|
|
de99221ad5 | ||
|
|
6892ce56ac | ||
|
|
41cea6f62e | ||
|
|
4bbffa97df | ||
|
|
614f8abfef | ||
|
|
14289b5fd1 | ||
|
|
4164beff1c | ||
|
|
9b3ab486de | ||
|
|
232a49814a | ||
|
|
6c61f0b135 | ||
|
|
c572cec253 | ||
|
|
d4941f2a5f | ||
|
|
bf5ec2f65f | ||
|
|
f8e21584b6 | ||
|
|
3cba83f84b | ||
|
|
dcb4255d7e | ||
|
|
9fc3c0dc2f | ||
|
|
a78830b48e | ||
|
|
949fbadcdc | ||
|
|
12c9e63b13 | ||
|
|
157b230702 | ||
|
|
c69299d659 | ||
|
|
285d630770 | ||
|
|
b9318092f4 | ||
|
|
905c361d52 | ||
|
|
4443abbc49 | ||
|
|
dabb36ad93 | ||
|
|
2bc8736fd9 | ||
|
|
e9b3b09cc2 | ||
|
|
ca337c32b4 | ||
|
|
24b8bd7c85 | ||
|
|
3ad75a441d | ||
|
|
f006e9be8d | ||
|
|
03f3ba8008 | ||
|
|
96a44eb7bf | ||
|
|
006782fe3d | ||
|
|
ff3e95bbf7 | ||
|
|
4b95a37e65 | ||
|
|
38c661b3a8 | ||
|
|
0d6e4f6a66 | ||
|
|
1ad2219f1c | ||
|
|
dfcdd289c3 | ||
|
|
32f5f2cca9 | ||
|
|
24bfe9f3e8 | ||
|
|
004667dc99 | ||
|
|
9d785dc781 | ||
|
|
cbba5a7af0 | ||
|
|
29b29ee349 | ||
|
|
9ad53e09af | ||
|
|
c9278991c9 | ||
|
|
729de48934 | ||
|
|
7476bff5fb | ||
|
|
1e9eae8d9a | ||
|
|
8113a53381 | ||
|
|
72833686f1 | ||
|
|
096c21f105 | ||
|
|
181f66357b | ||
|
|
a54fbc483c | ||
|
|
92d5a002d3 | ||
|
|
f9184903d7 | ||
|
|
91cde6ce7b | ||
|
|
186a4587c7 | ||
|
|
6049acb1f3 | ||
|
|
2d2ebf313c | ||
|
|
3d329dcb52 | ||
|
|
06854fc34f | ||
|
|
e01e14d866 | ||
|
|
3dfd251ebb | ||
|
|
dcea807f77 | ||
|
|
87d83ff33a | ||
|
|
1d31cbdf0d |
@@ -3,4 +3,5 @@ docs/
|
||||
.gitignore
|
||||
debug
|
||||
config/
|
||||
*.pyc
|
||||
*.pyc
|
||||
.git
|
||||
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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]
|
||||
|
||||
28
.github/workflows/push.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: On push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-0.8.0
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm install
|
||||
- name: Build docs
|
||||
run: npm run build
|
||||
- name: Deploy documentation
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/build
|
||||
11
.gitignore
vendored
@@ -1,4 +1,11 @@
|
||||
*.pyc
|
||||
.DS_Store
|
||||
*.pyc
|
||||
debug
|
||||
.vscode
|
||||
config/config.yml
|
||||
config/config.yml
|
||||
models
|
||||
*.mp4
|
||||
*.db
|
||||
frigate/version.py
|
||||
web/build
|
||||
web/node_modules
|
||||
|
||||
36
Makefile
@@ -1,49 +1,59 @@
|
||||
default_target: amd64_frigate
|
||||
|
||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||
|
||||
version:
|
||||
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
|
||||
|
||||
web:
|
||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||
|
||||
amd64_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:amd64 --file docker/Dockerfile.wheels .
|
||||
docker build --tag blakeblackshear/frigate-wheels:1.0.1-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 web
|
||||
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.amd64 .
|
||||
|
||||
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
|
||||
|
||||
amd64nvidia_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:amd64nvidia --file docker/Dockerfile.wheels .
|
||||
docker build --tag blakeblackshear/frigate-wheels:1.0.1-amd64nvidia --file docker/Dockerfile.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 web
|
||||
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
|
||||
|
||||
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
|
||||
|
||||
aarch64_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:aarch64 --file docker/Dockerfile.wheels.aarch64 .
|
||||
docker build --tag blakeblackshear/frigate-wheels:1.0.1-aarch64 --file docker/Dockerfile.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 web
|
||||
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.aarch64 .
|
||||
|
||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||
|
||||
armv7_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:armv7 --file docker/Dockerfile.wheels .
|
||||
docker build --tag blakeblackshear/frigate-wheels:1.0.1-armv7 --file docker/Dockerfile.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 web
|
||||
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.armv7 .
|
||||
|
||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||
|
||||
.PHONY: web
|
||||
|
||||
906
README.md
@@ -1,8 +1,9 @@
|
||||
<p align="center">
|
||||
<img align="center" alt="logo" src="docs/frigate.png">
|
||||
<img align="center" alt="logo" src="docs/static/img/frigate.png">
|
||||
</p>
|
||||
|
||||
# Frigate - NVR With Realtime Object Detection for IP Cameras
|
||||
|
||||
A complete and local NVR designed for HomeAssistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
||||
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but highly recommended. The Coral will outperform even the best CPUs and can process 100+ FPS with very little overhead.
|
||||
@@ -13,900 +14,25 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
|
||||
- Uses a very low overhead motion detection to determine where to run object detection
|
||||
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
||||
- Communicates over MQTT for easy integration into other systems
|
||||
- Records video clips of detected objects
|
||||
- 24/7 recording
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
|
||||
## Documentation
|
||||
|
||||
View the documentation at https://blakeblackshear.github.io/frigate
|
||||
|
||||
## Screenshots
|
||||
Integration into HomeAssistant
|
||||
<div>
|
||||
<a href="docs/media_browser.png"><img src="docs/media_browser.png" height=400></a>
|
||||
<a href="docs/notification.png"><img src="docs/notification.png" height=400></a>
|
||||
<a href="docs/static/img/media_browser.png"><img src="docs/static/img/media_browser.png" height=400></a>
|
||||
<a href="docs/static/img/notification.png"><img src="docs/static/img/notification.png" height=400></a>
|
||||
</div>
|
||||
|
||||
## Documentation
|
||||
- [How Frigate Works](docs/how-frigate-works.md)
|
||||
- [Recommended Hardware](#recommended-hardware)
|
||||
- [Installing](#installing)
|
||||
- [Configuration File](#configuration)
|
||||
- [Setting Up Camera Inputs](#setting-up-camera-inputs)
|
||||
- [Optimizing Performance](#optimizing-performance)
|
||||
- [Detectors](#detectors)
|
||||
- [Object Filters](#object-filters)
|
||||
- [Masks](#masks)
|
||||
- [Zones](#zones)
|
||||
- [Recording Clips](#recording-clips)
|
||||
- [24/7 Recordings](#247-recordings)
|
||||
- [RTMP Streams](#rtmp-streams)
|
||||
- [Integration with HomeAssistant](#integration-with-homeassistant)
|
||||
- [MQTT Topics](#mqtt-topics)
|
||||
- [HTTP Endpoints](#http-endpoints)
|
||||
- [Custom Models](#custom-models)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Recommended Hardware
|
||||
|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.|
|
||||
|Intel NUC NUC7i3BNK|8-10ms|Great performance. Can handle many cameras at 5fps depending on typical amounts of motion.|
|
||||
|BMAX B2 Plus|10-12ms|Good balance of performance and cost. Also capable of running many other services at the same time as frigate.|
|
||||
|Minisforum GK41|9-10ms|Great alternative to a NUC with dual Gigabit NICs. Easily handles several 1080p cameras.|
|
||||
|Raspberry Pi 3B (32bit)|60ms|Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0.|
|
||||
|Raspberry Pi 4 (32bit)|15-20ms|Can handle a small number of cameras. The 2GB version runs fine.|
|
||||
|Raspberry Pi 4 (64bit)|10-15ms|Can handle a small number of cameras. The 2GB version runs fine.|
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Installing
|
||||
|
||||
### HassOS Addon
|
||||
HassOS users can install via the addon repository. Frigate requires that an MQTT server be running.
|
||||
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
|
||||
1. Start the addon container
|
||||
|
||||
### Docker
|
||||
Make sure you choose the right image for your architecture:
|
||||
|Arch|Image Name|
|
||||
|-|-|
|
||||
|amd64|blakeblackshear/frigate:stable-amd64|
|
||||
|armv7|blakeblackshear/frigate:stable-armv7|
|
||||
|aarch64|blakeblackshear/frigate:stable-aarch64|
|
||||
|
||||
It is recommended to run with docker-compose:
|
||||
```yaml
|
||||
frigate:
|
||||
container_name: frigate
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
image: blakeblackshear/frigate:stable-amd64
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- <path_to_config>:/config
|
||||
- <path_to_directory_for_clips>:/media/frigate/clips
|
||||
- <path_to_directory_for_recordings>:/media/frigate/recordings
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 100000000
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "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:
|
||||
```bash
|
||||
docker run --rm \
|
||||
--name frigate \
|
||||
--privileged \
|
||||
-v /dev/bus/usb:/dev/bus/usb \
|
||||
-v <path_to_config_dir>:/config:ro \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-p 5000:5000 \
|
||||
-e FRIGATE_RTSP_PASSWORD='password' \
|
||||
blakeblackshear/frigate:stable-amd64
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
Use the [helm chart](https://github.com/k8s-at-home/charts/tree/master/charts/frigate).
|
||||
|
||||
### Virtualization
|
||||
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
||||
|
||||
#### Proxmox
|
||||
Some people have had success running Frigate in LXC directly with the following config:
|
||||
```
|
||||
arch: amd64
|
||||
cores: 2
|
||||
features: nesting=1
|
||||
hostname: FrigateLXC
|
||||
memory: 4096
|
||||
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=2E:76:AE:5A:58:48,ip=dhcp,ip6=auto,type=veth
|
||||
ostype: debian
|
||||
rootfs: local-lvm:vm-115-disk-0,size=12G
|
||||
swap: 512
|
||||
lxc.cgroup.devices.allow: c 189:385 rwm
|
||||
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/bus/usb/004/002 dev/bus/usb/004/002 none bind,optional,create=file
|
||||
lxc.apparmor.profile: unconfined
|
||||
lxc.cgroup.devices.allow: a
|
||||
lxc.cap.drop:
|
||||
```
|
||||
|
||||
### Calculating shm-size
|
||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
```
|
||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Configuration
|
||||
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](docs/cameras.md).
|
||||
|
||||
It is recommended to start with a minimal configuration and add to it:
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
cameras:
|
||||
back:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
height: 720
|
||||
width: 1280
|
||||
fps: 5
|
||||
```
|
||||
Here are all the configuration options:
|
||||
```yaml
|
||||
# Optional: Logging configuration
|
||||
logger:
|
||||
# Optional: default log level (default: shown below)
|
||||
default: info
|
||||
# Optional: module by module log level configuration
|
||||
logs:
|
||||
frigate.mqtt: error
|
||||
|
||||
# Optional: detectors configuration
|
||||
# USB Coral devices will be auto detected with CPU fallback
|
||||
detectors:
|
||||
# Required: name of the detector
|
||||
coral:
|
||||
# Required: type of the detector
|
||||
# Valid values are 'edgetpu' (requires device property below) and 'cpu'.
|
||||
type: edgetpu
|
||||
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
|
||||
device: usb
|
||||
|
||||
# Required: mqtt configuration
|
||||
mqtt:
|
||||
# Required: host name
|
||||
host: mqtt.server.com
|
||||
# Optional: port (default: shown below)
|
||||
port: 1883
|
||||
# Optional: topic prefix (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
topic_prefix: frigate
|
||||
# Optional: client id (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
client_id: frigate
|
||||
# Optional: user
|
||||
user: mqtt_user
|
||||
# Optional: password
|
||||
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
|
||||
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
|
||||
password: password
|
||||
|
||||
# Optional: Global configuration for saving clips
|
||||
save_clips:
|
||||
# Optional: Maximum length of time to retain video during long events. (default: shown below)
|
||||
# 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: Retention settings for clips (default: shown below)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: Global ffmpeg args
|
||||
# Args may be provided as a string or an array
|
||||
# "ffmpeg" + global_args + input_args + "-i" + input + output_args
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel fatal
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
# Optional: global input args (default: shown below)
|
||||
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
|
||||
# Optional: global output args
|
||||
output_args:
|
||||
# Optional: output args for detect streams (default: shown below)
|
||||
detect: -f rawvideo -pix_fmt yuv420p
|
||||
# Optional: output args for record streams (default: shown below)
|
||||
record: -f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
# Optional: output args for clips streams (default: shown below)
|
||||
clips: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
# Optional: output args for rtmp streams (default: shown below)
|
||||
rtmp: -c copy -f flv
|
||||
|
||||
# Optional: Global object filters for all cameras.
|
||||
# NOTE: can be overridden at the camera level
|
||||
objects:
|
||||
# Optional: list of objects to track from labelmap.txt (default: shown below)
|
||||
track:
|
||||
- person
|
||||
# Optional: filters to reduce false positives for specific object types
|
||||
filters:
|
||||
person:
|
||||
# Optional: minimum width*height of the bounding box for the detected object (default: 0)
|
||||
min_area: 5000
|
||||
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
|
||||
max_area: 100000
|
||||
# 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
|
||||
|
||||
# Required: configuration section for cameras
|
||||
cameras:
|
||||
# Required: name of the camera
|
||||
back:
|
||||
# Required: ffmpeg settings for the camera
|
||||
ffmpeg:
|
||||
# Required: A list of input streams for the camera. See documentation for more information.
|
||||
inputs:
|
||||
# Required: the path to the stream
|
||||
# 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
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: stream specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: stream specific input args (default: inherit)
|
||||
input_args:
|
||||
|
||||
# Optional: camera specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: camera specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: camera specific input args (default: inherit)
|
||||
input_args:
|
||||
# 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.
|
||||
width: 1280
|
||||
# Optional: desired fps for your camera
|
||||
# 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
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
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
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# Required: List of x,y coordinates to define the polygon of the zone.
|
||||
# NOTE: Coordinates can be generated at https://www.image-map.net/
|
||||
coordinates: 545,1077,747,939,788,805
|
||||
# Optional: Zone level object filters.
|
||||
# NOTE: The global and camera filters are applied upstream.
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.8
|
||||
|
||||
# Optional: save clips configuration
|
||||
# NOTE: This feature does not work if you have added "-vsync drop" in your input params.
|
||||
# This will only work for camera feeds that can be copied into the mp4 container format without
|
||||
# encoding such as h264. It may not work for some types of streams.
|
||||
save_clips:
|
||||
# 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
|
||||
# Optional: Objects to save clips for. (default: all tracked objects)
|
||||
objects:
|
||||
- person
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: 24/7 recording configuration
|
||||
record:
|
||||
# Optional: Enable recording (default: global setting)
|
||||
enabled: False
|
||||
# Optional: Number of days to retain (default: global setting)
|
||||
retain_days: 30
|
||||
|
||||
# Optional: RTMP re-stream configuration
|
||||
rtmp:
|
||||
# Required: Enable the live stream (default: True)
|
||||
enabled: True
|
||||
|
||||
# Optional: Configuration for the snapshots in the debug view and mqtt
|
||||
snapshots:
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
show_timestamp: True
|
||||
# Optional: draw zones on the debug mjpeg feed (default: shown below)
|
||||
draw_zones: False
|
||||
# Optional: draw bounding boxes on the mqtt snapshots (default: shown below)
|
||||
draw_bounding_boxes: True
|
||||
# Optional: crop the snapshot to the detection region (default: shown below)
|
||||
crop_to_region: True
|
||||
# Optional: height to resize the snapshot to (default: shown below)
|
||||
# NOTE: 175px is optimized for thumbnails in the homeassistant media browser
|
||||
height: 175
|
||||
|
||||
# Optional: Camera level object filters config. If defined, this is used instead of the global config.
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
min_score: 0.5
|
||||
threshold: 0.85
|
||||
```
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Setting Up Camera Inputs
|
||||
Up to 4 inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create clips from a higher resolution stream, or vice versa.
|
||||
|
||||
Each role can only be assigned to one input per camera. The options for roles are as follows:
|
||||
|Role|Description|
|
||||
|----|-----|
|
||||
|`detect`|Main feed for object detection|
|
||||
|`clips`|Clips of events from objects detected in the `detect` feed. [docs](#recording-clips)|
|
||||
|`record`|Saves 60 second segments of the video feed. [docs](#247-recordings)|
|
||||
|`rtmp`|Broadcast as an RTMP feed for other services to consume. [docs](#rtmp-streams)|
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
cameras:
|
||||
back:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live
|
||||
roles:
|
||||
- clips
|
||||
- record
|
||||
height: 720
|
||||
width: 1280
|
||||
fps: 5
|
||||
```
|
||||
|
||||
|
||||
[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.
|
||||
- **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.
|
||||
- **Masks**: Masks can be used to ignore motion and reduce your idle CPU load. If you have areas with regular motion such as timestamps or trees blowing in the wind, frigate will constantly try to determine if that motion is from a person or other object you are tracking. Those detections not only increase your average CPU usage, but also clog the pipeline for detecting objects elsewhere. If you are experiencing high values for `detection_fps` when no objects of interest are in the cameras, you should use masks to tell frigate to ignore movement from trees, bushes, timestamps, or any part of the image where detections should not be wasted looking for objects.
|
||||
|
||||
### 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):
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_mmal
|
||||
```
|
||||
|
||||
Raspberry Pi 3/4 (64-bit OS)
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_v4l2m2m
|
||||
```
|
||||
|
||||
Intel-based CPUs (<10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
- -hwaccel_output_format
|
||||
- yuv420p
|
||||
```
|
||||
|
||||
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
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
Nvidia GPU based decoding via NVDEC is supported, but requires special configuration. See the [nvidia NVDEC documentation](docs/nvdec.md) for more details.
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Detectors
|
||||
By default Frigate will look for a USB Coral device and fall back to the CPU if it cannot be found. If you have PCI or multiple Coral devices, you need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
|
||||
|
||||
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).
|
||||
|
||||
Single USB Coral:
|
||||
```yaml
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
```
|
||||
|
||||
Multiple USB Corals:
|
||||
```yaml
|
||||
detectors:
|
||||
coral1:
|
||||
type: edgetpu
|
||||
device: usb:0
|
||||
coral2:
|
||||
type: edgetpu
|
||||
device: usb:1
|
||||
```
|
||||
|
||||
Mixing Corals:
|
||||
```yaml
|
||||
detectors:
|
||||
coral_usb:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
coral_pci:
|
||||
type: edgetpu
|
||||
device: pci
|
||||
```
|
||||
|
||||
CPU Detectors (not recommended):
|
||||
```yaml
|
||||
detectors:
|
||||
cpu1:
|
||||
type: cpu
|
||||
cpu2:
|
||||
type: cpu
|
||||
```
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Reducing False Positives
|
||||
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_score`, `threshold`.
|
||||
|
||||
For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:
|
||||
|
||||
| Frame | Current Score | Score History | Computed Score | Detected Object |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No
|
||||
| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No
|
||||
| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No
|
||||
| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes
|
||||
| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes
|
||||
| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes
|
||||
|
||||
In frame 2, the score is below the `min_score` value, so frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example.
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Masks
|
||||
The following types of masks are supported:
|
||||
- `poly`: (Recommended) List of x,y points like zone configuration
|
||||
- `base64`: Base64 encoded image file
|
||||
- `image`: Image file in the `/config` directory
|
||||
|
||||
`base64` and `image` masks must be the same aspect ratio and resolution as your camera.
|
||||
|
||||
The mask in the second image would limit motion detection on this camera to only the front yard and not the street.
|
||||
|
||||
<a href="docs/example-mask-check-point.png"><img src="docs/example-mask-check-point.png" height="300"></a>
|
||||
<a href="docs/example-mask.bmp"><img src="docs/example-mask.bmp" height="300"></a>
|
||||
<a href="docs/example-mask-overlay.png"><img src="docs/example-mask-overlay.png" height="300"></a>
|
||||
|
||||
To create a poly mask:
|
||||
1. Download a camera snapshot image with the same resolution as the camera feed (`/<camera_name>/latest.jpg`).
|
||||
1. Upload the image to https://www.image-map.net/
|
||||
1. Select "shape" poly - start in the lowest left corner and place the first marker (point) and continue upwards and then to the right until the polygon shape covers the area that you want to mask out (ignore).
|
||||
1. When you are finished with the polygon click "Show me the code!" and copy all coordinates (point), ie. `"0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432"`
|
||||
1. Adjust any -1 values to 0 and then add it all to the configuration (see the example configuration for correct indentation and placement)
|
||||
|
||||
Example of a finished row corresponding to the below example image:
|
||||
```yaml
|
||||
mask: 'poly,0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432'
|
||||
```
|
||||
|
||||
<a href="docs/example-mask-poly.png"><img src="docs/example-mask-poly.png" height="300"></a>
|
||||
|
||||
You can test your mask by temporarily configuring it as a [zone](#zones) and enabling `draw_zones` in your config. Zones are visible on the [MJPEG feed](#camera_name).
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Zones
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
|
||||
|
||||
During testing, `draw_zones` should be set in the config to draw the zone on the frames so you can adjust as needed. The zone line will increase in thickness when any object enters the zone. Zones are visible on the [MJPEG feed](#camera_name).
|
||||
|
||||

|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Recording Clips
|
||||
**Note**: Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
|
||||
|
||||
Frigate can save video clips without any CPU overhead for encoding by simply copying the stream directly with FFmpeg. It leverages FFmpeg's segment functionality to maintain a cache of video for each camera. The cache files are written to disk at `/tmp/cache` and do not introduce memory overhead. When an object is being tracked, it will extend the cache to ensure it can assemble a clip when the event ends. Once the event ends, it again uses FFmpeg to assemble a clip by combining the video clips without any encoding by the CPU. Assembled clips are are saved to `/media/frigate/clips`. Clips are retained according to the retention settings defined on the config for each object type.
|
||||
|
||||
### 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.
|
||||
|
||||
### Global Configuration Options
|
||||
- `max_seconds`: This limits the size of the cache when an object is being tracked. If an object is stationary and being tracked for a long time, the cache files will expire and this value will be the maximum clip length for the *end* of the event. For example, if this is set to 300 seconds and an object is being tracked for 600 seconds, the clip will end up being the last 300 seconds. Defaults to 300 seconds.
|
||||
|
||||
### Per-camera Configuration Options
|
||||
- `pre_capture`: Defines how much time should be included in the clip prior to the beginning of the event. Defaults to 30 seconds.
|
||||
- `objects`: List of object types to save clips for. Object types here must be listed for tracking at the camera or global configuration. Defaults to all tracked objects.
|
||||
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## 24/7 Recordings
|
||||
**Note**: Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
|
||||
|
||||
24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in HomeAssistant's media browser. Each camera supports a configurable retention policy in the config.
|
||||
|
||||
[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.
|
||||
|
||||
[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:
|
||||
|
||||
Sensors:
|
||||
- Stats to monitor frigate performance
|
||||
- Object counts for all zones and cameras
|
||||
|
||||
Cameras:
|
||||
- Cameras for image of the last detected object for each camera
|
||||
- Camera entities with stream support (requires RTMP)
|
||||
|
||||
Media Browser:
|
||||
- Rich UI with thumbnails for browsing event clips
|
||||
- Rich UI for browsing 24/7 recordings by month, day, camera, time
|
||||
|
||||
API:
|
||||
- Notification API with public facing endpoints for images in notifications
|
||||
|
||||
### Notifications
|
||||
Frigate publishes event information in the form of a change feed via MQTT. This allows lots of customization for notifications to meet your needs. Event changes are published with `before` and `after` information as shown [here](#frigateevents).
|
||||
|
||||
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}.jpg?format=android'
|
||||
tag: '{{trigger.payload_json["after"]["id"]}}'
|
||||
```
|
||||
Note that the image url has `?format=android`. This adjusts the aspect ratio to be idea for android notifications. For iOS optimized snapshots, no format parameter needs to be passed.
|
||||
|
||||
You can find some additional examples for notifications [here](docs/notification-examples.md).
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## HTTP Endpoints
|
||||
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`
|
||||
|
||||
### `/<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.
|
||||
|
||||
Example parameters:
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
|
||||
|
||||
### `/<camera_name>/latest.jpg[?h=300]`
|
||||
The most recent frame that frigate has finished processing. It is a full resolution image by default.
|
||||
|
||||
Example parameters:
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
|
||||
### `/stats`
|
||||
Contains some granular debug info that can be used for sensors in HomeAssistant.
|
||||
|
||||
Sample response:
|
||||
```jsonc
|
||||
{
|
||||
/* Per Camera Stats */
|
||||
"back": {
|
||||
/***************
|
||||
* Frames per second being consumed from your camera. If this is higher
|
||||
* than it is supposed to be, you should set -r FPS in your input_args.
|
||||
* camera_fps = process_fps + skipped_fps
|
||||
***************/
|
||||
"camera_fps": 5.0,
|
||||
/***************
|
||||
* Number of times detection is run per second. This can be higher than
|
||||
* your camera FPS because frigate often looks at the same frame multiple times
|
||||
* or in multiple locations
|
||||
***************/
|
||||
"detection_fps": 1.5,
|
||||
/***************
|
||||
* PID for the ffmpeg process that consumes this camera
|
||||
***************/
|
||||
"capture_pid": 27,
|
||||
/***************
|
||||
* PID for the process that runs detection for this camera
|
||||
***************/
|
||||
"pid": 34,
|
||||
/***************
|
||||
* Frames per second being processed by frigate.
|
||||
***************/
|
||||
"process_fps": 5.1,
|
||||
/***************
|
||||
* Frames per second skip for processing by frigate.
|
||||
***************/
|
||||
"skipped_fps": 0.0
|
||||
},
|
||||
/***************
|
||||
* Sum of detection_fps across all cameras and detectors.
|
||||
* This should be the sum of all detection_fps values from cameras.
|
||||
***************/
|
||||
"detection_fps": 5.0,
|
||||
/* Detectors Stats */
|
||||
"detectors": {
|
||||
"coral": {
|
||||
/***************
|
||||
* Timestamp when object detection started. If this value stays non-zero and constant
|
||||
* for a long time, that means the detection process is stuck.
|
||||
***************/
|
||||
"detection_start": 0.0,
|
||||
/***************
|
||||
* Time spent running object detection in milliseconds.
|
||||
***************/
|
||||
"inference_speed": 10.48,
|
||||
/***************
|
||||
* PID for the shared process that runs object detection on the Coral.
|
||||
***************/
|
||||
"pid": 25321
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `/config`
|
||||
A json representation of your configuration
|
||||
|
||||
### `/events`
|
||||
Events from the database. Accepts the following query string parameters:
|
||||
|param|Type|Description|
|
||||
|----|-----|--|
|
||||
|`before`|int|Epoch time|
|
||||
|`after`|int|Epoch time|
|
||||
|`camera`|str|Camera name|
|
||||
|`label`|str|Label name|
|
||||
|`zone`|str|Zone name|
|
||||
|`limit`|int|Limit the number of events returned|
|
||||
|
||||
### `/events/summary`
|
||||
Returns summary data for events in the database. Used by the HomeAssistant integration.
|
||||
|
||||
### `/events/<id>`
|
||||
Returns data for a single event.
|
||||
### `/events/<id>/snapshot.jpg`
|
||||
Returns a snapshot for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## MQTT Topics
|
||||
These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file.
|
||||
|
||||
### `frigate/available`
|
||||
Designed to be used as an availability topic with HomeAssistant. Possible message are:
|
||||
"online": published when frigate is running (on startup)
|
||||
"offline": published right before frigate stops
|
||||
|
||||
### `frigate/<camera_name>/<object_name>`
|
||||
Publishes the count of objects for the camera for use as a sensor in HomeAssistant.
|
||||
|
||||
### `frigate/<zone_name>/<object_name>`
|
||||
Publishes the count of objects for the zone for use as a sensor in HomeAssistant.
|
||||
|
||||
### `frigate/<camera_name>/<object_name>/snapshot`
|
||||
Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image
|
||||
is published again.
|
||||
|
||||
The height and crop of snapshots can be configured in the config.
|
||||
|
||||
### `frigate/events`
|
||||
Message published for each changed event:
|
||||
```json
|
||||
{
|
||||
"before": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123961.837752,
|
||||
"label": "person",
|
||||
"top_score": 0.958984375,
|
||||
"false_positive": false,
|
||||
"start_time": 1607123955.475377,
|
||||
"end_time": null,
|
||||
"score": 0.7890625,
|
||||
"box": [
|
||||
424,
|
||||
500,
|
||||
536,
|
||||
712
|
||||
],
|
||||
"area": 23744,
|
||||
"region": [
|
||||
264,
|
||||
450,
|
||||
667,
|
||||
853
|
||||
],
|
||||
"current_zones": [
|
||||
"driveway"
|
||||
],
|
||||
"entered_zones": [
|
||||
"yard",
|
||||
"driveway"
|
||||
],
|
||||
"thumbnail": null
|
||||
},
|
||||
"after": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123962.082975,
|
||||
"label": "person",
|
||||
"top_score": 0.958984375,
|
||||
"false_positive": false,
|
||||
"start_time": 1607123955.475377,
|
||||
"end_time": null,
|
||||
"score": 0.87890625,
|
||||
"box": [
|
||||
432,
|
||||
496,
|
||||
544,
|
||||
854
|
||||
],
|
||||
"area": 40096,
|
||||
"region": [
|
||||
218,
|
||||
440,
|
||||
693,
|
||||
915
|
||||
],
|
||||
"current_zones": [
|
||||
"yard",
|
||||
"driveway"
|
||||
],
|
||||
"entered_zones": [
|
||||
"yard",
|
||||
"driveway"
|
||||
],
|
||||
"thumbnail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Custom Models
|
||||
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
|
||||
- CPU Model: `/cpu_model.tflite`
|
||||
- EdgeTPU Model: `/edgetpu_model.tflite`
|
||||
- Labels: `/labelmap.txt`
|
||||
|
||||
### 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:
|
||||
|
||||
- Download the [COCO labelmap](https://dl.google.com/coral/canned_models/coco_labels.txt)
|
||||
- Modify the label names as desired. For example, change `7 truck` to `7 car`
|
||||
- Mount the new file at `/labelmap.txt` in the container with an additional volume
|
||||
```
|
||||
-v ./config/labelmap.txt:/labelmap.txt
|
||||
```
|
||||
|
||||
[Back to top](#documentation)
|
||||
|
||||
## Logging
|
||||
Available log levels are: `debug`, `info`, `warning`, `error`, `critical`
|
||||
|
||||
Examples of available modules are:
|
||||
- `frigate.app`
|
||||
- `frigate.mqtt`
|
||||
- `frigate.edgetpu`
|
||||
- `frigate.zeroconf`
|
||||
- `detector.<detector_name>`
|
||||
- `watchdog.<camera_name>`
|
||||
- `ffmpeg.<camera_name>.<sorted_roles>` NOTE: All FFmpeg logs are sent as `error` level.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "ffmpeg didnt return a frame. something is wrong"
|
||||
Turn on logging for the camera by overriding the global_args and setting the log level to `info`:
|
||||
```yaml
|
||||
ffmpeg:
|
||||
global_args:
|
||||
- -hide_banner
|
||||
- -loglevel
|
||||
- info
|
||||
```
|
||||
|
||||
### "On connect called"
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
|
||||
[Back to top](#documentation)
|
||||
Also comes with a builtin UI:
|
||||
<div>
|
||||
<a href="docs/static/img/home-ui.png"><img src="docs/static/img/home-ui.png" height=400></a>
|
||||
<a href="docs/static/img/camera-ui.png"><img src="docs/static/img/camera-ui.png" height=400></a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
ARG ARCH=amd64
|
||||
FROM blakeblackshear/frigate-wheels:${ARCH} as wheels
|
||||
FROM blakeblackshear/frigate-ffmpeg:1.0.0-${ARCH} as ffmpeg
|
||||
ARG WHEELS_VERSION
|
||||
ARG FFMPEG_VERSION
|
||||
FROM blakeblackshear/frigate-wheels:${WHEELS_VERSION}-${ARCH} as wheels
|
||||
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
|
||||
FROM frigate-web as web
|
||||
|
||||
FROM ubuntu:20.04
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
@@ -29,20 +32,22 @@ RUN apt-get -qq update \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
|
||||
RUN pip3 install \
|
||||
peewee \
|
||||
peewee_migrate \
|
||||
zeroconf \
|
||||
voluptuous
|
||||
|
||||
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/
|
||||
ADD migrations migrations/
|
||||
|
||||
COPY --from=web /opt/frigate/build web/
|
||||
|
||||
COPY run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
@@ -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 \
|
||||
|
||||
9
docker/Dockerfile.web
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG NODE_VERSION=14.0
|
||||
|
||||
FROM node:${NODE_VERSION}
|
||||
|
||||
WORKDIR /opt/frigate
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install && npm run build
|
||||
@@ -18,13 +18,14 @@ RUN apt-get -qq update \
|
||||
gcc gfortran libopenblas-dev liblapack-dev cython
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py
|
||||
&& python3 get-pip.py "pip==20.2.4"
|
||||
|
||||
RUN pip3 install scikit-build
|
||||
|
||||
RUN pip3 wheel --wheel-dir=/wheels \
|
||||
opencv-python-headless \
|
||||
numpy \
|
||||
# pinning due to issue in 1.19.5 https://github.com/numpy/numpy/issues/18131
|
||||
numpy==1.19.4 \
|
||||
imutils \
|
||||
scipy \
|
||||
psutil \
|
||||
@@ -32,7 +33,9 @@ RUN pip3 wheel --wheel-dir=/wheels \
|
||||
paho-mqtt \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
click
|
||||
click \
|
||||
setproctitle \
|
||||
peewee
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
FROM ubuntu:20.04 as build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
wget \
|
||||
# opencv dependencies
|
||||
build-essential cmake git pkg-config libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||
# scipy dependencies
|
||||
gcc gfortran libopenblas-dev liblapack-dev cython
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py
|
||||
|
||||
# need to build cmake from source because binary distribution is broken for arm64
|
||||
# https://github.com/scikit-build/cmake-python-distributions/issues/115
|
||||
# https://github.com/skvark/opencv-python/issues/366
|
||||
# https://github.com/scikit-build/cmake-python-distributions/issues/96#issuecomment-663062358
|
||||
RUN pip3 install scikit-build
|
||||
|
||||
RUN git clone https://github.com/scikit-build/cmake-python-distributions.git \
|
||||
&& cd cmake-python-distributions/ \
|
||||
&& python3 setup.py bdist_wheel
|
||||
|
||||
RUN pip3 install cmake-python-distributions/dist/*.whl
|
||||
|
||||
RUN pip3 wheel --wheel-dir=/wheels \
|
||||
opencv-python-headless \
|
||||
numpy \
|
||||
imutils \
|
||||
scipy \
|
||||
psutil \
|
||||
Flask \
|
||||
paho-mqtt \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
click
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /wheels /wheels
|
||||
20
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
33
docs/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
|
||||
|
||||
## Installation
|
||||
|
||||
```console
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```console
|
||||
yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```console
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
## Deployment
|
||||
|
||||
```console
|
||||
GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
3
docs/babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
# Camera Specific Configuration
|
||||
Frigate should work with most RTSP cameras and h264 feeds such as Dahua.
|
||||
|
||||
## RTMP Cameras
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- '1'
|
||||
```
|
||||
139
docs/docs/configuration/advanced.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
id: advanced
|
||||
title: Advanced
|
||||
sidebar_label: Advanced
|
||||
---
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
### `motion`
|
||||
|
||||
Global motion detection config. These may also be defined at the camera level.
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
|
||||
### `detect`
|
||||
|
||||
Global object detection settings. These may also be defined at the camera level.
|
||||
|
||||
```yaml
|
||||
detect:
|
||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: double the frame rate)
|
||||
max_disappeared: 10
|
||||
```
|
||||
|
||||
### `logger`
|
||||
|
||||
Change the default log level for troubleshooting purposes.
|
||||
|
||||
```yaml
|
||||
logger:
|
||||
# Optional: default log level (default: shown below)
|
||||
default: info
|
||||
# Optional: module by module log level configuration
|
||||
logs:
|
||||
frigate.mqtt: error
|
||||
```
|
||||
|
||||
Available log levels are: `debug`, `info`, `warning`, `error`, `critical`
|
||||
|
||||
Examples of available modules are:
|
||||
|
||||
- `frigate.app`
|
||||
- `frigate.mqtt`
|
||||
- `frigate.edgetpu`
|
||||
- `frigate.zeroconf`
|
||||
- `detector.<detector_name>`
|
||||
- `watchdog.<camera_name>`
|
||||
- `ffmpeg.<camera_name>.<sorted_roles>` NOTE: All FFmpeg logs are sent as `error` level.
|
||||
|
||||
### `environment_vars`
|
||||
|
||||
This section can be used to set environment variables for those unable to modify the environment of the container (ie. within Hass.io)
|
||||
|
||||
```yaml
|
||||
environment_vars:
|
||||
EXAMPLE_VAR: value
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
This may need to be in a custom location if network storage is used for clips.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /media/frigate/clips/frigate.db
|
||||
```
|
||||
|
||||
### `detectors`
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
# Required: name of the detector
|
||||
coral:
|
||||
# Required: type of the detector
|
||||
# Valid values are 'edgetpu' (requires device property below) and 'cpu'. 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
|
||||
```
|
||||
|
||||
### `model`
|
||||
|
||||
```yaml
|
||||
model:
|
||||
# Required: height of the trained model
|
||||
height: 320
|
||||
# Required: width of the trained model
|
||||
width: 320
|
||||
```
|
||||
|
||||
## Custom Models
|
||||
|
||||
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
|
||||
|
||||
- CPU Model: `/cpu_model.tflite`
|
||||
- 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:
|
||||
|
||||
- Download the [COCO labelmap](https://dl.google.com/coral/canned_models/coco_labels.txt)
|
||||
- Modify the label names as desired. For example, change `7 truck` to `7 car`
|
||||
- Mount the new file at `/labelmap.txt` in the container with an additional volume
|
||||
```
|
||||
-v ./config/labelmap.txt:/labelmap.txt
|
||||
```
|
||||
410
docs/docs/configuration/cameras.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
id: cameras
|
||||
title: Cameras
|
||||
---
|
||||
|
||||
## Setting Up Camera Inputs
|
||||
|
||||
Up to 4 inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create clips from a higher resolution stream, or vice versa.
|
||||
|
||||
Each role can only be assigned to one input per camera. The options for roles are as follows:
|
||||
|
||||
| Role | Description |
|
||||
| -------- | ------------------------------------------------------------------------------------ |
|
||||
| `detect` | Main feed for object detection |
|
||||
| `clips` | Clips of events from objects detected in the `detect` feed. [docs](#recording-clips) |
|
||||
| `record` | Saves 60 second segments of the video feed. [docs](#247-recordings) |
|
||||
| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](#rtmp-streams) |
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
cameras:
|
||||
back:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live
|
||||
roles:
|
||||
- clips
|
||||
- record
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
```
|
||||
|
||||
## Masks & Zones
|
||||
|
||||
### Masks
|
||||
Masks are used to ignore initial detection in areas of your camera's field of view.
|
||||
|
||||
There are two types of masks available:
|
||||
- **Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the video feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
|
||||
- **Object filter masks**: Object filter masks are used to filter out false positives for a given object type. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
|
||||
|
||||
To create a poly mask:
|
||||
|
||||
1. Visit the [web UI](/usage/web)
|
||||
1. Click the camera you wish to create a mask for
|
||||
1. Click "Mask & Zone creator"
|
||||
1. Click "Add" on the type of mask or zone you would like to create
|
||||
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
|
||||
1. When you've finished creating your mask, click "Copy" and paste the contents into your `config.yaml` file and restart Frigate
|
||||
|
||||
Example of a finished row corresponding to the below example image:
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask: '0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432'
|
||||
```
|
||||
|
||||

|
||||
|
||||
```yaml
|
||||
# Optional: camera level motion config
|
||||
motion:
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 0,900,1080,900,1080,1920,0,1920
|
||||
```
|
||||
|
||||
### Zones
|
||||
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
|
||||
|
||||
During testing, `draw_zones` should be set in the config to draw the zone on the frames so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
|
||||
|
||||
To create a zone, follow the same steps above for a "Motion mask", but use the section of the web UI for creating a zone instead.
|
||||
|
||||
```yaml
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# Required: List of x,y coordinates to define the polygon of the zone.
|
||||
# NOTE: Coordinates can be generated at https://www.image-map.net/
|
||||
coordinates: 545,1077,747,939,788,805
|
||||
# Optional: Zone level object filters.
|
||||
# NOTE: The global and camera filters are applied upstream.
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.7
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
```yaml
|
||||
# Optional: Camera level object filters config.
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
min_score: 0.5
|
||||
threshold: 0.7
|
||||
# Optional: mask to prevent this object type from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
```
|
||||
|
||||
## Clips
|
||||
|
||||
Frigate can save video clips without any CPU overhead for encoding by simply copying the stream directly with FFmpeg. It leverages FFmpeg's segment functionality to maintain a cache of video for each camera. The cache files are written to disk at `/tmp/cache` and do not introduce memory overhead. When an object is being tracked, it will extend the cache to ensure it can assemble a clip when the event ends. Once the event ends, it again uses FFmpeg to assemble a clip by combining the video clips without any encoding by the CPU. Assembled clips are are saved to `/media/frigate/clips`. Clips are retained according to the retention settings defined on the config for each object type.
|
||||
|
||||
:::caution
|
||||
Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
|
||||
:::
|
||||
|
||||
```yaml
|
||||
clips:
|
||||
# Required: enables clips for the camera (default: shown below)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: Number of seconds before the event to include in the clips (default: shown below)
|
||||
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
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
## Snapshots
|
||||
|
||||
Frigate can save a snapshot image to `/media/frigate/clips` for each event named as `<camera>-<id>.jpg`.
|
||||
|
||||
```yaml
|
||||
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
|
||||
snapshots:
|
||||
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: False
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: False
|
||||
# Optional: height to resize the snapshot to (default: original size)
|
||||
height: 175
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
## 24/7 Recordings
|
||||
|
||||
24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in HomeAssistant's media browser. Each camera supports a configurable retention policy in the config.
|
||||
|
||||
:::caution
|
||||
Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
|
||||
:::
|
||||
|
||||
```yaml
|
||||
# Optional: 24/7 recording configuration
|
||||
record:
|
||||
# Optional: Enable recording (default: global setting)
|
||||
enabled: False
|
||||
# Optional: Number of days to retain (default: global setting)
|
||||
retain_days: 30
|
||||
```
|
||||
|
||||
## RTMP streams
|
||||
|
||||
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.
|
||||
|
||||
## Full example
|
||||
|
||||
The following is a full example of all of the options together for a camera configuration
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
# Required: name of the camera
|
||||
back:
|
||||
# Required: ffmpeg settings for the camera
|
||||
ffmpeg:
|
||||
# Required: A list of input streams for the camera. See documentation for more information.
|
||||
inputs:
|
||||
# Required: the path to the stream
|
||||
# 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
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: stream specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: stream specific input args (default: inherit)
|
||||
input_args:
|
||||
|
||||
# Optional: camera specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: camera specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: camera specific input args (default: inherit)
|
||||
input_args:
|
||||
# Optional: camera specific output args (default: inherit)
|
||||
output_args:
|
||||
|
||||
# Required: width of the frame for the input with the detect role
|
||||
width: 1280
|
||||
# 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: camera level motion config
|
||||
motion:
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 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: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# Required: List of x,y coordinates to define the polygon of the zone.
|
||||
# NOTE: Coordinates can be generated at https://www.image-map.net/
|
||||
coordinates: 545,1077,747,939,788,805
|
||||
# Optional: Zone level object filters.
|
||||
# NOTE: The global and camera filters are applied upstream.
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.7
|
||||
|
||||
# Optional: Camera level detect settings
|
||||
detect:
|
||||
# Optional: enables detection for the camera (default: True)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: True
|
||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: double the frame rate)
|
||||
max_disappeared: 10
|
||||
|
||||
# Optional: save clips configuration
|
||||
clips:
|
||||
# Required: enables clips for the camera (default: shown below)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: Number of seconds before the event to include in the clips (default: shown below)
|
||||
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
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: 24/7 recording configuration
|
||||
record:
|
||||
# Optional: Enable recording (default: global setting)
|
||||
enabled: False
|
||||
# Optional: Number of days to retain (default: global setting)
|
||||
retain_days: 30
|
||||
|
||||
# Optional: RTMP re-stream configuration
|
||||
rtmp:
|
||||
# Required: Enable the live stream (default: True)
|
||||
enabled: True
|
||||
|
||||
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
|
||||
snapshots:
|
||||
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: False
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: False
|
||||
# Optional: height to resize the snapshot to (default: original size)
|
||||
height: 175
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: Configuration for the jpg snapshots published via MQTT
|
||||
mqtt:
|
||||
# Optional: Enable publishing snapshot via mqtt for camera (default: shown below)
|
||||
# NOTE: Only applies to publishing image data to MQTT via 'frigate/<camera_name>/<object_name>/snapshot'.
|
||||
# All other messages will still be published.
|
||||
enabled: True
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: True
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: True
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: True
|
||||
# Optional: height to resize the snapshot to (default: shown below)
|
||||
height: 270
|
||||
|
||||
# Optional: Camera level object filters config.
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
min_score: 0.5
|
||||
threshold: 0.7
|
||||
# Optional: mask to prevent this object type from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
```
|
||||
|
||||
## Camera specific configuration
|
||||
|
||||
### RTMP Cameras
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- '1'
|
||||
```
|
||||
|
||||
### Blue Iris RTSP Cameras
|
||||
|
||||
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -rtsp_transport
|
||||
- tcp
|
||||
- -stimeout
|
||||
- '5000000'
|
||||
- -use_wallclock_as_timestamps
|
||||
- '1'
|
||||
```
|
||||
53
docs/docs/configuration/detectors.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
id: detectors
|
||||
title: Detectors
|
||||
---
|
||||
|
||||
The default config will look for a USB Coral device. If you do not have a Coral, you will need to configure a CPU detector. If you have PCI or multiple Coral devices, you need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
|
||||
|
||||
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:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
```
|
||||
|
||||
Multiple USB Corals:
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral1:
|
||||
type: edgetpu
|
||||
device: usb:0
|
||||
coral2:
|
||||
type: edgetpu
|
||||
device: usb:1
|
||||
```
|
||||
|
||||
Mixing Corals:
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral_usb:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
coral_pci:
|
||||
type: edgetpu
|
||||
device: pci
|
||||
```
|
||||
|
||||
CPU Detectors (not recommended):
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
cpu1:
|
||||
type: cpu
|
||||
cpu2:
|
||||
type: cpu
|
||||
```
|
||||
19
docs/docs/configuration/false_positives.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
id: false_positives
|
||||
title: Reducing false positives
|
||||
---
|
||||
|
||||
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_score`, `threshold`.
|
||||
|
||||
For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:
|
||||
|
||||
| Frame | Current Score | Score History | Computed Score | Detected Object |
|
||||
| ----- | ------------- | --------------------------------- | -------------- | --------------- |
|
||||
| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No |
|
||||
| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No |
|
||||
| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No |
|
||||
| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes |
|
||||
| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes |
|
||||
| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes |
|
||||
|
||||
In frame 2, the score is below the `min_score` value, so frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example.
|
||||
137
docs/docs/configuration/index.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
id: index
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
HassOS users can manage their configuration directly in the addon Configuration tab. For other installations, the default location for the config file is `/config/config.yml`. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](/configuration/cameras.md).
|
||||
|
||||
It is recommended to start with a minimal configuration and add to it:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
cameras:
|
||||
back:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
```
|
||||
|
||||
## Required
|
||||
|
||||
## `mqtt`
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
# Required: host name
|
||||
host: mqtt.server.com
|
||||
# Optional: port (default: shown below)
|
||||
port: 1883
|
||||
# Optional: topic prefix (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
topic_prefix: frigate
|
||||
# Optional: client id (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
client_id: frigate
|
||||
# Optional: user
|
||||
user: mqtt_user
|
||||
# Optional: password
|
||||
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
|
||||
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
|
||||
password: password
|
||||
# Optional: interval in seconds for publishing stats (default: shown below)
|
||||
stats_interval: 60
|
||||
```
|
||||
|
||||
## `cameras`
|
||||
|
||||
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras) for a complete list of options.
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
# Name of your camera
|
||||
front_door:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
```
|
||||
|
||||
## Optional
|
||||
|
||||
### `clips`
|
||||
|
||||
```yaml
|
||||
clips:
|
||||
# Optional: Maximum length of time to retain video during long events. (default: shown below)
|
||||
# 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)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
### `ffmpeg`
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel fatal
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
# Optional: global input args (default: shown below)
|
||||
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
|
||||
# Optional: global output args
|
||||
output_args:
|
||||
# Optional: output args for detect streams (default: shown below)
|
||||
detect: -f rawvideo -pix_fmt yuv420p
|
||||
# Optional: output args for record streams (default: shown below)
|
||||
record: -f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
# Optional: output args for clips streams (default: shown below)
|
||||
clips: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
# Optional: output args for rtmp streams (default: shown below)
|
||||
rtmp: -c copy -f flv
|
||||
```
|
||||
|
||||
### `objects`
|
||||
|
||||
Can be overridden at the camera level
|
||||
|
||||
```yaml
|
||||
objects:
|
||||
# Optional: list of objects to track from labelmap.txt (default: shown below)
|
||||
track:
|
||||
- person
|
||||
# Optional: filters to reduce false positives for specific object types
|
||||
filters:
|
||||
person:
|
||||
# Optional: minimum width*height of the bounding box for the detected object (default: 0)
|
||||
min_area: 5000
|
||||
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
|
||||
max_area: 100000
|
||||
# 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.7
|
||||
```
|
||||
@@ -1,14 +1,18 @@
|
||||
# nVidia hardware decoder (NVDEC)
|
||||
---
|
||||
id: nvdec
|
||||
title: nVidia hardware decoder
|
||||
---
|
||||
|
||||
Certain nvidia cards include a hardware decoder, which can greatly improve the
|
||||
performance of video decoding. In order to use NVDEC, a special build of
|
||||
performance of video decoding. In order to use NVDEC, a special build of
|
||||
ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvidia'
|
||||
includes this support for amd64 platforms. An aarch64 for the Jetson, which
|
||||
includes this support for amd64 platforms. An aarch64 for the Jetson, which
|
||||
also includes NVDEC may be added in the future.
|
||||
|
||||
## Docker setup
|
||||
|
||||
### Requirements
|
||||
|
||||
[nVidia closed source driver](https://www.nvidia.com/en-us/drivers/unix/) required to access NVDEC.
|
||||
[nvidia-docker](https://github.com/NVIDIA/nvidia-docker) required to pass NVDEC to docker.
|
||||
|
||||
@@ -18,6 +22,7 @@ In order to pass NVDEC, the docker engine must be set to `nvidia` and the enviro
|
||||
`NVIDIA_VISIBLE_DEVICES=all` and `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` must be set.
|
||||
|
||||
In a docker compose file, these lines need to be set:
|
||||
|
||||
```
|
||||
services:
|
||||
frigate:
|
||||
@@ -26,15 +31,16 @@ services:
|
||||
runtime: nvidia
|
||||
environment:
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
|
||||
```
|
||||
|
||||
### Setting up the configuration file
|
||||
|
||||
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
|
||||
The decoder you choose will depend on the input video.
|
||||
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
|
||||
The decoder you choose will depend on the input video.
|
||||
|
||||
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
|
||||
|
||||
```
|
||||
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
|
||||
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
|
||||
@@ -46,7 +52,7 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c
|
||||
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
|
||||
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
|
||||
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
|
||||
```
|
||||
```
|
||||
|
||||
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
|
||||
`-c:v hevc_covid` to your ffmpeg input arguments:
|
||||
@@ -55,7 +61,7 @@ For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
|
||||
ffmpeg:
|
||||
input_args:
|
||||
...
|
||||
- -c:v
|
||||
- -c:v
|
||||
- hevc_cuvid
|
||||
```
|
||||
|
||||
@@ -75,7 +81,7 @@ processes:
|
||||
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
|
||||
| | | N/A |
|
||||
+-------------------------------+----------------------+----------------------+
|
||||
|
||||
|
||||
+-----------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
@@ -96,10 +102,9 @@ using the fps filter:
|
||||
|
||||
```
|
||||
output_args:
|
||||
- -filter:v
|
||||
- -filter:v
|
||||
- fps=fps=5
|
||||
```
|
||||
|
||||
This setting, for example, allows Frigate to consume my 10-15fps camera streams on
|
||||
my relatively low powered Haswell machine with relatively low cpu usage.
|
||||
|
||||
72
docs/docs/configuration/optimizing.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
id: optimizing
|
||||
title: 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. 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.
|
||||
- **Masks**: Masks can be used to ignore motion and reduce your idle CPU load. If you have areas with regular motion such as timestamps or trees blowing in the wind, frigate will constantly try to determine if that motion is from a person or other object you are tracking. Those detections not only increase your average CPU usage, but also clog the pipeline for detecting objects elsewhere. If you are experiencing high values for `detection_fps` when no objects of interest are in the cameras, you should use masks to tell frigate to ignore movement from trees, bushes, timestamps, or any part of the image where detections should not be wasted looking for objects.
|
||||
|
||||
### 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)
|
||||
**NOTICE**: If you are using the addon, ensure you turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_mmal
|
||||
```
|
||||
|
||||
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:
|
||||
- -c:v
|
||||
- h264_v4l2m2m
|
||||
```
|
||||
|
||||
Intel-based CPUs (<10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
- -hwaccel_output_format
|
||||
- yuv420p
|
||||
```
|
||||
|
||||
Intel-based CPUs (>=10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
|
||||
```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:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
Nvidia GPU based decoding via NVDEC is supported, but requires special configuration. See the [nvidia NVDEC documentation](/configuration/nvdec) for more details.
|
||||
20
docs/docs/hardware.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
id: hardware
|
||||
title: 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. |
|
||||
| Intel NUC NUC7i3BNK | 8-10ms | Great performance. Can handle many cameras at 5fps depending on typical amounts of motion. |
|
||||
| BMAX B2 Plus | 10-12ms | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
|
||||
| Minisforum GK41 | 9-10ms | Great alternative to a NUC with dual Gigabit NICs. Easily handles several 1080p cameras. |
|
||||
| Raspberry Pi 3B (32bit) | 60ms | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. |
|
||||
| Raspberry Pi 4 (32bit) | 15-20ms | Can handle a small number of cameras. The 2GB version runs fine. |
|
||||
| Raspberry Pi 4 (64bit) | 10-15ms | Can handle a small number of cameras. The 2GB version runs fine. |
|
||||
13
docs/docs/how-it-works.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
id: how-it-works
|
||||
title: How Frigate Works
|
||||
sidebar_label: How it works
|
||||
---
|
||||
|
||||
Frigate is designed to minimize resource and maximize performance by only looking for objects when and where it is necessary
|
||||
|
||||

|
||||
|
||||
1. Look for Motion
|
||||
2. Calculate Detection Regions
|
||||
3. Run Object Detection
|
||||
25
docs/docs/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: index
|
||||
title: Frigate
|
||||
sidebar_label: Features
|
||||
slug: /
|
||||
---
|
||||
|
||||
A complete and local NVR designed for HomeAssistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
||||
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but highly recommended. The Coral will outperform even the best CPUs and can process 100+ FPS with very little overhead.
|
||||
|
||||
- Tight integration with HomeAssistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
||||
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
||||
- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame
|
||||
- Uses a very low overhead motion detection to determine where to run object detection
|
||||
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
||||
- Communicates over MQTT for easy integration into other systems
|
||||
- 24/7 recording
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
115
docs/docs/installation.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: installation
|
||||
title: Installation
|
||||
---
|
||||
|
||||
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 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
|
||||
1. Start the addon container
|
||||
|
||||
## Docker
|
||||
|
||||
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:0.8.0-beta2-amd64
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- <path_to_config>:/config
|
||||
- <path_to_directory_for_clips>:/media/frigate/clips
|
||||
- <path_to_directory_for_recordings>:/media/frigate/recordings
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
ports:
|
||||
- '5000:5000'
|
||||
- '1935:1935' # RTMP feeds
|
||||
environment:
|
||||
FRIGATE_RTSP_PASSWORD: 'password'
|
||||
```
|
||||
|
||||
If you can't use docker compose, you can run the container with something similar to this:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
--name frigate \
|
||||
--privileged \
|
||||
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \
|
||||
-v /dev/bus/usb:/dev/bus/usb \
|
||||
-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 \
|
||||
-e FRIGATE_RTSP_PASSWORD='password' \
|
||||
-p 5000:5000 \
|
||||
-p 1935:1935 \
|
||||
blakeblackshear/frigate:0.8.0-beta2-amd64
|
||||
```
|
||||
|
||||
## Kubernetes
|
||||
|
||||
Use the [helm chart](https://github.com/k8s-at-home/charts/tree/master/charts/frigate).
|
||||
|
||||
## Virtualization
|
||||
|
||||
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
||||
|
||||
## Proxmox
|
||||
|
||||
Some people have had success running Frigate in LXC directly with the following config:
|
||||
|
||||
```
|
||||
arch: amd64
|
||||
cores: 2
|
||||
features: nesting=1
|
||||
hostname: FrigateLXC
|
||||
memory: 4096
|
||||
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=2E:76:AE:5A:58:48,ip=dhcp,ip6=auto,type=veth
|
||||
ostype: debian
|
||||
rootfs: local-lvm:vm-115-disk-0,size=12G
|
||||
swap: 512
|
||||
lxc.cgroup.devices.allow: c 189:385 rwm
|
||||
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/bus/usb/004/002 dev/bus/usb/004/002 none bind,optional,create=file
|
||||
lxc.apparmor.profile: unconfined
|
||||
lxc.cgroup.devices.allow: a
|
||||
lxc.cap.drop:
|
||||
```
|
||||
|
||||
### Calculating shm-size
|
||||
|
||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
17
docs/docs/mdx.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
id: mdx
|
||||
title: Powered by MDX
|
||||
---
|
||||
|
||||
You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/).
|
||||
|
||||
export const Highlight = ({children, color}) => ( <span style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
padding: '0.2rem',
|
||||
}}>{children}</span> );
|
||||
|
||||
<Highlight color="#25c2a0">Docusaurus green</Highlight> and <Highlight color="#1877F2">Facebook blue</Highlight> are my favorite colors.
|
||||
|
||||
I can write **Markdown** alongside my _JSX_!
|
||||
26
docs/docs/troubleshooting.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: troubleshooting
|
||||
title: Troubleshooting
|
||||
---
|
||||
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
||||
|
||||

|
||||
|
||||
## "[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 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
|
||||
```
|
||||
|
||||
## "On connect called"
|
||||
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
179
docs/docs/usage/api.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
id: api
|
||||
title: HTTP API
|
||||
---
|
||||
|
||||
A web server is available on port 5000 with the following endpoints.
|
||||
|
||||
### `/api/<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.
|
||||
|
||||
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`.
|
||||
|
||||
### `/api/<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.
|
||||
|
||||
Example parameters:
|
||||
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
|
||||
|
||||
### `/api/<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
|
||||
|
||||
### `/api/stats`
|
||||
|
||||
Contains some granular debug info that can be used for sensors in HomeAssistant.
|
||||
|
||||
Sample response:
|
||||
|
||||
```json
|
||||
{
|
||||
/* Per Camera Stats */
|
||||
"back": {
|
||||
/***************
|
||||
* Frames per second being consumed from your camera. If this is higher
|
||||
* than it is supposed to be, you should set -r FPS in your input_args.
|
||||
* camera_fps = process_fps + skipped_fps
|
||||
***************/
|
||||
"camera_fps": 5.0,
|
||||
/***************
|
||||
* Number of times detection is run per second. This can be higher than
|
||||
* your camera FPS because frigate often looks at the same frame multiple times
|
||||
* or in multiple locations
|
||||
***************/
|
||||
"detection_fps": 1.5,
|
||||
/***************
|
||||
* PID for the ffmpeg process that consumes this camera
|
||||
***************/
|
||||
"capture_pid": 27,
|
||||
/***************
|
||||
* PID for the process that runs detection for this camera
|
||||
***************/
|
||||
"pid": 34,
|
||||
/***************
|
||||
* Frames per second being processed by frigate.
|
||||
***************/
|
||||
"process_fps": 5.1,
|
||||
/***************
|
||||
* Frames per second skip for processing by frigate.
|
||||
***************/
|
||||
"skipped_fps": 0.0
|
||||
},
|
||||
/***************
|
||||
* Sum of detection_fps across all cameras and detectors.
|
||||
* This should be the sum of all detection_fps values from cameras.
|
||||
***************/
|
||||
"detection_fps": 5.0,
|
||||
/* Detectors Stats */
|
||||
"detectors": {
|
||||
"coral": {
|
||||
/***************
|
||||
* Timestamp when object detection started. If this value stays non-zero and constant
|
||||
* for a long time, that means the detection process is stuck.
|
||||
***************/
|
||||
"detection_start": 0.0,
|
||||
/***************
|
||||
* Time spent running object detection in milliseconds.
|
||||
***************/
|
||||
"inference_speed": 10.48,
|
||||
/***************
|
||||
* PID for the shared process that runs object detection on the Coral.
|
||||
***************/
|
||||
"pid": 25321
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
/* Uptime in seconds */
|
||||
"uptime": 10,
|
||||
"version": "0.8.0-8883709"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `/api/config`
|
||||
|
||||
A json representation of your configuration
|
||||
|
||||
### `/api/version`
|
||||
|
||||
Version info
|
||||
|
||||
### `/api/events`
|
||||
|
||||
Events from the database. Accepts the following query string parameters:
|
||||
|
||||
| param | Type | Description |
|
||||
| -------------- | ---- | --------------------------------------------- |
|
||||
| `before` | int | Epoch time |
|
||||
| `after` | int | Epoch time |
|
||||
| `camera` | str | Camera name |
|
||||
| `label` | str | Label name |
|
||||
| `zone` | str | Zone name |
|
||||
| `limit` | int | Limit the number of events returned |
|
||||
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
|
||||
| `has_clip` | int | Filter to events that have clips (0 or 1) |
|
||||
|
||||
### `/api/events/summary`
|
||||
|
||||
Returns summary data for events in the database. Used by the HomeAssistant integration.
|
||||
|
||||
### `/api/events/<id>`
|
||||
|
||||
Returns data for a single event.
|
||||
|
||||
### `/api/events/<id>/thumbnail.jpg`
|
||||
|
||||
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||
|
||||
### `/api/events/<id>/snapshot.jpg`
|
||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
||||
|
||||
Accepts the following query string parameters, but they are only applied when an event is in progress. After the event is completed, the saved snapshot is returned from disk without modification:
|
||||
|
||||
|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)|
|
||||
|`crop`|int|Crop the snapshot to the (0 or 1)|
|
||||
|
||||
### `/clips/<camera>-<id>.mp4`
|
||||
|
||||
Video clip for the given camera and event id.
|
||||
|
||||
### `/clips/<camera>-<id>.jpg`
|
||||
|
||||
JPG snapshot for the given camera and event id.
|
||||
120
docs/docs/usage/home-assistant.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
id: home-assistant
|
||||
title: Integration with Home Assistant
|
||||
sidebar_label: Home Assistant
|
||||
---
|
||||
|
||||
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
|
||||
- Object counts for all zones and cameras
|
||||
|
||||
## Cameras:
|
||||
|
||||
- Cameras for image of the last detected object for each camera
|
||||
- Camera entities with stream support (requires RTMP)
|
||||
|
||||
## Media Browser:
|
||||
|
||||
- Rich UI with thumbnails for browsing event clips
|
||||
- Rich UI for browsing 24/7 recordings by month, day, camera, time
|
||||
|
||||
## API:
|
||||
|
||||
- Notification API with public facing endpoints for images in notifications
|
||||
|
||||
### Notifications
|
||||
|
||||
Frigate publishes event information in the form of a change feed via MQTT. This allows lots of customization for notifications to meet your needs. Event changes are published with `before` and `after` information as shown [here](#frigateevents).
|
||||
|
||||
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
|
||||
tag: '{{trigger.payload_json["after"]["id"]}}'
|
||||
```
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: When a person enters a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
conditions:
|
||||
- "{{ 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."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- 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'] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- 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 }}"
|
||||
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']}}/thumbnail.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/api/events/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg'
|
||||
caption: 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
|
||||
```
|
||||
99
docs/docs/usage/mqtt.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
id: mqtt
|
||||
title: MQTT
|
||||
---
|
||||
|
||||
These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file.
|
||||
|
||||
### `frigate/available`
|
||||
|
||||
Designed to be used as an availability topic with HomeAssistant. Possible message are:
|
||||
"online": published when frigate is running (on startup)
|
||||
"offline": published right before frigate stops
|
||||
|
||||
### `frigate/<camera_name>/<object_name>`
|
||||
|
||||
Publishes the count of objects for the camera for use as a sensor in HomeAssistant.
|
||||
|
||||
### `frigate/<zone_name>/<object_name>`
|
||||
|
||||
Publishes the count of objects for the zone for use as a sensor in HomeAssistant.
|
||||
|
||||
### `frigate/<camera_name>/<object_name>/snapshot`
|
||||
|
||||
Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image
|
||||
is published again.
|
||||
|
||||
The height and crop of snapshots can be configured in the config.
|
||||
|
||||
### `frigate/events`
|
||||
|
||||
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.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "update", // new, update, or end
|
||||
"before": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123961.837752,
|
||||
"label": "person",
|
||||
"top_score": 0.958984375,
|
||||
"false_positive": false,
|
||||
"start_time": 1607123955.475377,
|
||||
"end_time": null,
|
||||
"score": 0.7890625,
|
||||
"box": [424, 500, 536, 712],
|
||||
"area": 23744,
|
||||
"region": [264, 450, 667, 853],
|
||||
"current_zones": ["driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null
|
||||
},
|
||||
"after": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123962.082975,
|
||||
"label": "person",
|
||||
"top_score": 0.958984375,
|
||||
"false_positive": false,
|
||||
"start_time": 1607123955.475377,
|
||||
"end_time": null,
|
||||
"score": 0.87890625,
|
||||
"box": [432, 496, 544, 854],
|
||||
"area": 40096,
|
||||
"region": [218, 440, 693, 915],
|
||||
"current_zones": ["yard", "driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `frigate/stats`
|
||||
|
||||
Same data available at `/api/stats` published at a configurable interval.
|
||||
|
||||
### `frigate/<camera_name>/detect/set`
|
||||
|
||||
Topic to turn detection for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/detect/state`
|
||||
|
||||
Topic with current state of detection for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/clips/set`
|
||||
|
||||
Topic to turn clips for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/clips/state`
|
||||
|
||||
Topic with current state of clips for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/snapshots/set`
|
||||
|
||||
Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/snapshots/state`
|
||||
|
||||
Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`.
|
||||
10
docs/docs/usage/web.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
id: web
|
||||
title: Web Interface
|
||||
---
|
||||
|
||||
Frigate comes bundled with a simple web ui that supports the following:
|
||||
|
||||
- Show cameras
|
||||
- Browse events
|
||||
- Mask helper
|
||||
76
docs/docusaurus.config.js
Normal file
@@ -0,0 +1,76 @@
|
||||
module.exports = {
|
||||
title: 'Frigate',
|
||||
tagline: 'NVR With Realtime Object Detection for IP Cameras',
|
||||
url: 'https://blakeblackshear.github.io',
|
||||
baseUrl: '/frigate/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
organizationName: 'blakeblackshear',
|
||||
projectName: 'frigate',
|
||||
themeConfig: {
|
||||
algolia: {
|
||||
apiKey: '81ec882db78f7fed05c51daf973f0362',
|
||||
indexName: 'frigate'
|
||||
},
|
||||
navbar: {
|
||||
title: 'Frigate',
|
||||
logo: {
|
||||
alt: 'Frigate',
|
||||
src: 'img/logo.svg',
|
||||
srcDark: 'img/logo-dark.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
to: '/',
|
||||
activeBasePath: 'docs',
|
||||
label: 'Docs',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/blakeblackshear/frigate',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
sidebarCollapsible: false,
|
||||
hideableSidebar: true,
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/blakeblackshear/frigate',
|
||||
},
|
||||
{
|
||||
label: 'Discussions',
|
||||
href: 'https://github.com/blakeblackshear/frigate/discussions',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Blake Blackshear`,
|
||||
},
|
||||
},
|
||||
presets: [
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
docs: {
|
||||
routeBasePath: '/',
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
editUrl: 'https://github.com/blakeblackshear/frigate/edit/master/docs/',
|
||||
},
|
||||
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 6.0 MiB |
@@ -1,10 +0,0 @@
|
||||
# How Frigate Works
|
||||
Frigate is designed to minimize resource and maximize performance by only looking for objects when and where it is necessary
|
||||
|
||||

|
||||
|
||||
## 1. Look for Motion
|
||||
|
||||
## 2. Calculate Detection Regions
|
||||
|
||||
## 3. Run Object Detection
|
||||
@@ -1,52 +0,0 @@
|
||||
# Notification examples
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
|
||||
- alias: When a person enters a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
conditions:
|
||||
- "{{ 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.'
|
||||
data:
|
||||
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"] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
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"]}}'
|
||||
|
||||
- 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 }}"
|
||||
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"]}}'
|
||||
```
|
||||
14035
docs/package-lock.json
generated
Normal file
34
docs/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"serve": "docusaurus serve",
|
||||
"clear": "docusaurus clear"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-alpha.70",
|
||||
"@docusaurus/preset-classic": "2.0.0-alpha.70",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"clsx": "^1.1.1",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
docs/sidebars.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
docs: {
|
||||
Frigate: ['index', 'how-it-works', 'hardware', 'installation', 'troubleshooting'],
|
||||
Configuration: [
|
||||
'configuration/index',
|
||||
'configuration/cameras',
|
||||
'configuration/optimizing',
|
||||
'configuration/detectors',
|
||||
'configuration/false_positives',
|
||||
'configuration/advanced',
|
||||
],
|
||||
Usage: ['usage/home-assistant', 'usage/web', 'usage/api', 'usage/mqtt'],
|
||||
},
|
||||
};
|
||||
25
docs/src/css/custom.css
Normal file
@@ -0,0 +1,25 @@
|
||||
/* stylelint-disable docusaurus/copyright-header */
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #3b82f7;
|
||||
--ifm-color-primary-dark: #1d4ed8;
|
||||
--ifm-color-primary-darker: #1e40af;
|
||||
--ifm-color-primary-darkest: #1e3a8a;
|
||||
--ifm-color-primary-light: #60a5fa;
|
||||
--ifm-color-primary-lighter: #93c5fd;
|
||||
--ifm-color-primary-lightest: #dbeafe;
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgb(72, 77, 91);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
0
docs/static/.nojekyll
vendored
Normal file
BIN
docs/static/img/camera-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 944 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/events-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/example-mask-poly.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
docs/static/img/home-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
3
docs/static/img/logo-dark.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.4999 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 936 B |
3
docs/static/img/logo.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.5 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 781 KiB After Width: | Height: | Size: 781 KiB |
BIN
docs/static/img/mismatched-resolution.jpg
vendored
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 73 KiB |
@@ -8,7 +8,9 @@ import sys
|
||||
import signal
|
||||
|
||||
import yaml
|
||||
from peewee_migrate import Router
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
@@ -20,6 +22,7 @@ from frigate.models import Event
|
||||
from frigate.mqtt import create_mqtt_client
|
||||
from frigate.object_processing import TrackedObjectProcessor
|
||||
from frigate.record import RecordingMaintainer
|
||||
from frigate.stats import StatsEmitter, stats_init
|
||||
from frigate.video import capture_camera, track_camera
|
||||
from frigate.watchdog import FrigateWatchdog
|
||||
from frigate.zeroconf import broadcast_zeroconf
|
||||
@@ -37,6 +40,10 @@ class FrigateApp():
|
||||
self.log_queue = mp.Queue()
|
||||
self.camera_metrics = {}
|
||||
|
||||
def set_environment_vars(self):
|
||||
for key, value in self.config.environment_vars.items():
|
||||
os.environ[key] = value
|
||||
|
||||
def ensure_dirs(self):
|
||||
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
|
||||
if not os.path.exists(d) and not os.path.islink(d):
|
||||
@@ -44,6 +51,13 @@ class FrigateApp():
|
||||
os.makedirs(d)
|
||||
else:
|
||||
logger.debug(f"Skipping directory: {d}")
|
||||
|
||||
tmpfs_size = self.config.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}")
|
||||
|
||||
def init_logger(self):
|
||||
self.log_process = mp.Process(target=log_process, args=(self.log_queue,), name='log_process')
|
||||
@@ -61,12 +75,31 @@ class FrigateApp():
|
||||
'camera_fps': mp.Value('d', 0.0),
|
||||
'skipped_fps': mp.Value('d', 0.0),
|
||||
'process_fps': mp.Value('d', 0.0),
|
||||
'detection_enabled': mp.Value('i', self.config.cameras[camera_name].detect.enabled),
|
||||
'detection_fps': mp.Value('d', 0.0),
|
||||
'detection_frame': mp.Value('d', 0.0),
|
||||
'read_start': mp.Value('d', 0.0),
|
||||
'ffmpeg_pid': mp.Value('i', 0),
|
||||
'frame_queue': mp.Queue(maxsize=2)
|
||||
'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.clips.enabled and 'clips' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has clips assigned to an input, but clips is not enabled.")
|
||||
elif camera.clips.enabled and not 'clips' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has 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,30 +118,42 @@ 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')}")
|
||||
migrate_db = SqliteExtDatabase(self.config.database.path)
|
||||
|
||||
# Run migrations
|
||||
del(logging.getLogger('peewee_migrate').handlers[:])
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
|
||||
migrate_db.close()
|
||||
|
||||
self.db = SqliteQueueDatabase(self.config.database.path)
|
||||
models = [Event]
|
||||
self.db.bind(models)
|
||||
self.db.create_tables(models, safe=True)
|
||||
|
||||
def init_stats(self):
|
||||
self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
|
||||
|
||||
def init_web_server(self):
|
||||
self.flask_app = create_app(self.config, self.db, self.camera_metrics, self.detectors, self.detected_frames_processor)
|
||||
self.flask_app = create_app(self.config, self.db, self.stats_tracking, self.detected_frames_processor)
|
||||
|
||||
def init_mqtt(self):
|
||||
self.mqtt_client = create_mqtt_client(self.config.mqtt)
|
||||
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
|
||||
|
||||
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 +161,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
|
||||
@@ -146,6 +192,10 @@ class FrigateApp():
|
||||
self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event)
|
||||
self.recording_maintainer.start()
|
||||
|
||||
def start_stats_emitter(self):
|
||||
self.stats_emitter = StatsEmitter(self.config, self.stats_tracking, self.mqtt_client, self.config.mqtt.topic_prefix, self.stop_event)
|
||||
self.stats_emitter.start()
|
||||
|
||||
def start_watchdog(self):
|
||||
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
|
||||
self.frigate_watchdog.start()
|
||||
@@ -153,29 +203,33 @@ 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}")
|
||||
print(f"Error parsing config: {e}")
|
||||
self.log_process.terminate()
|
||||
sys.exit(1)
|
||||
self.set_environment_vars()
|
||||
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()
|
||||
self.start_detected_frames_processor()
|
||||
self.start_camera_processors()
|
||||
self.start_camera_capture_processes()
|
||||
self.init_stats()
|
||||
self.init_web_server()
|
||||
self.start_event_processor()
|
||||
self.start_event_cleanup()
|
||||
self.start_recording_maintainer()
|
||||
self.start_stats_emitter()
|
||||
self.start_watchdog()
|
||||
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
|
||||
|
||||
@@ -196,6 +250,7 @@ class FrigateApp():
|
||||
self.event_processor.join()
|
||||
self.event_cleanup.join()
|
||||
self.recording_maintainer.join()
|
||||
self.stats_emitter.join()
|
||||
self.frigate_watchdog.join()
|
||||
|
||||
for detector in self.detectors.values():
|
||||
|
||||
@@ -8,6 +8,7 @@ import threading
|
||||
import signal
|
||||
from abc import ABC, abstractmethod
|
||||
from multiprocessing.connection import Connection
|
||||
from setproctitle import setproctitle
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
@@ -43,7 +44,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 = {}
|
||||
@@ -61,16 +62,15 @@ class LocalObjectDetector(ObjectDetector):
|
||||
logger.info(f"Attempting to load TPU as {device_config['device']}")
|
||||
edge_tpu_delegate = load_delegate('libedgetpu.so.1.0', device_config)
|
||||
logger.info("TPU found")
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path='/edgetpu_model.tflite',
|
||||
experimental_delegates=[edge_tpu_delegate])
|
||||
except ValueError:
|
||||
logger.info("No EdgeTPU detected. Falling back to CPU.")
|
||||
|
||||
if edge_tpu_delegate is None:
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path='/cpu_model.tflite')
|
||||
logger.info("No EdgeTPU detected.")
|
||||
raise
|
||||
else:
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path='/edgetpu_model.tflite',
|
||||
experimental_delegates=[edge_tpu_delegate])
|
||||
model_path='/cpu_model.tflite', num_threads=num_threads)
|
||||
|
||||
self.interpreter.allocate_tensors()
|
||||
|
||||
@@ -106,10 +106,11 @@ 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()}")
|
||||
setproctitle(f"frigate.detector.{name}")
|
||||
listen()
|
||||
|
||||
stop_event = mp.Event()
|
||||
@@ -120,7 +121,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 +140,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 +156,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 +181,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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -87,27 +88,35 @@ class EventProcessor(threading.Thread):
|
||||
earliest_event = datetime.datetime.now().timestamp()
|
||||
|
||||
# if the earliest event exceeds the max seconds, cap it
|
||||
max_seconds = self.config.save_clips.max_seconds
|
||||
max_seconds = self.config.clips.max_seconds
|
||||
if datetime.datetime.now().timestamp()-earliest_event > max_seconds:
|
||||
earliest_event = datetime.datetime.now().timestamp()-max_seconds
|
||||
|
||||
for f, data in list(self.cached_clips.items()):
|
||||
if earliest_event-90 > data['start_time']+data['duration']:
|
||||
del self.cached_clips[f]
|
||||
logger.debug(f"Cleaning up cached file {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']:
|
||||
# if there are no clips in the cache or we are still waiting on a needed file check every 5 seconds
|
||||
wait_count = 0
|
||||
while len(sorted_clips) == 0 or sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']+post_capture:
|
||||
if wait_count > 4:
|
||||
logger.warning(f"Unable to create clip for {camera} and event {event_data['id']}. There were no cache files for this event.")
|
||||
return False
|
||||
logger.debug(f"No cache clips for {camera}. Waiting...")
|
||||
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'])
|
||||
wait_count += 1
|
||||
|
||||
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,13 +147,16 @@ class EventProcessor(threading.Thread):
|
||||
'-',
|
||||
'-c',
|
||||
'copy',
|
||||
'-movflags',
|
||||
'+faststart',
|
||||
f"{os.path.join(CLIPS_DIR, clip_name)}.mp4"
|
||||
]
|
||||
|
||||
p = sp.run(ffmpeg_cmd, input="\n".join(playlist_lines), encoding='ascii', capture_output=True)
|
||||
if p.returncode != 0:
|
||||
logger.error(p.stderr)
|
||||
return
|
||||
return False
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
@@ -159,28 +171,20 @@ class EventProcessor(threading.Thread):
|
||||
self.refresh_cache()
|
||||
continue
|
||||
|
||||
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
|
||||
self.refresh_cache()
|
||||
|
||||
save_clips_config = self.config.cameras[camera].save_clips
|
||||
|
||||
# if save clips is not enabled for this camera, just continue
|
||||
if not save_clips_config.enabled:
|
||||
if event_type == 'end':
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
continue
|
||||
|
||||
# if specific objects are listed for this camera, only save clips for them
|
||||
if not event_data['label'] in save_clips_config.objects:
|
||||
if event_type == 'end':
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
continue
|
||||
|
||||
if event_type == 'start':
|
||||
self.events_in_process[event_data['id']] = event_data
|
||||
|
||||
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)
|
||||
clips_config = self.config.cameras[camera].clips
|
||||
|
||||
if not event_data['false_positive']:
|
||||
clip_created = False
|
||||
if clips_config.enabled and (clips_config.objects is None or event_data['label'] in clips_config.objects):
|
||||
clip_created = self.create_clip(camera, event_data, clips_config.pre_capture, clips_config.post_capture)
|
||||
|
||||
Event.create(
|
||||
id=event_data['id'],
|
||||
label=event_data['label'],
|
||||
@@ -190,7 +194,9 @@ class EventProcessor(threading.Thread):
|
||||
top_score=event_data['top_score'],
|
||||
false_positive=event_data['false_positive'],
|
||||
zones=list(event_data['entered_zones']),
|
||||
thumbnail=event_data['thumbnail']
|
||||
thumbnail=event_data['thumbnail'],
|
||||
has_clip=clip_created,
|
||||
has_snapshot=event_data['has_snapshot'],
|
||||
)
|
||||
del self.events_in_process[event_data['id']]
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
@@ -201,7 +207,86 @@ class EventCleanup(threading.Thread):
|
||||
self.name = 'event_cleanup'
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
self.camera_keys = list(self.config.cameras.keys())
|
||||
|
||||
def expire(self, media):
|
||||
## Expire events from unlisted cameras based on the global config
|
||||
if media == 'clips':
|
||||
retain_config = self.config.clips.retain
|
||||
file_extension = 'mp4'
|
||||
update_params = {'has_clip': False}
|
||||
else:
|
||||
retain_config = self.config.snapshots.retain
|
||||
file_extension = 'jpg'
|
||||
update_params = {'has_snapshot': False}
|
||||
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera.not_in(self.camera_keys))
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the media from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
|
||||
media.unlink(missing_ok=True)
|
||||
# update the clips attribute for the db entry
|
||||
update_query = (
|
||||
Event.update(update_params)
|
||||
.where(Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
## Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
if media == 'clips':
|
||||
retain_config = camera.clips.retain
|
||||
else:
|
||||
retain_config = camera.snapshots.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera == name)
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
|
||||
media.unlink(missing_ok=True)
|
||||
# update the clips attribute for the db entry
|
||||
update_query = (
|
||||
Event.update(update_params)
|
||||
.where( Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
while(True):
|
||||
@@ -216,71 +301,13 @@ class EventCleanup(threading.Thread):
|
||||
continue
|
||||
counter = 0
|
||||
|
||||
camera_keys = list(self.config.cameras.keys())
|
||||
self.expire('clips')
|
||||
self.expire('snapshots')
|
||||
|
||||
# Expire events from unlisted cameras based on the global config
|
||||
retain_config = self.config.save_clips.retain
|
||||
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera.not_in(camera_keys))
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera.not_in(camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
clip_name = f"{event.camera}-{event.id}"
|
||||
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4")
|
||||
clip.unlink(missing_ok=True)
|
||||
# delete the event for this type from the db
|
||||
delete_query = (
|
||||
Event.delete()
|
||||
.where(Event.camera.not_in(camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
# Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
retain_config = camera.save_clips.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera == name)
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
clip_name = f"{event.camera}-{event.id}"
|
||||
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4")
|
||||
clip.unlink(missing_ok=True)
|
||||
# delete the event for this type from the db
|
||||
delete_query = (
|
||||
Event.delete()
|
||||
.where( Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
delete_query.execute()
|
||||
# drop events from db where has_clip and has_snapshot are false
|
||||
delete_query = (
|
||||
Event.delete()
|
||||
.where( Event.has_clip == False,
|
||||
Event.has_snapshot == False)
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
163
frigate/http.py
@@ -12,13 +12,17 @@ from flask import (Blueprint, Flask, Response, current_app, jsonify,
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event
|
||||
from frigate.stats import stats_snapshot
|
||||
from frigate.util import calculate_region
|
||||
from frigate.version import VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('frigate', __name__)
|
||||
|
||||
def create_app(frigate_config, database: SqliteDatabase, camera_metrics, detectors, detected_frames_processor):
|
||||
def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detected_frames_processor):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.before_request
|
||||
@@ -31,10 +35,9 @@ def create_app(frigate_config, database: SqliteDatabase, camera_metrics, detecto
|
||||
database.close()
|
||||
|
||||
app.frigate_config = frigate_config
|
||||
app.camera_metrics = camera_metrics
|
||||
app.detectors = detectors
|
||||
app.stats_tracking = stats_tracking
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
|
||||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
return app
|
||||
@@ -45,18 +48,33 @@ def is_healthy():
|
||||
|
||||
@bp.route('/events/summary')
|
||||
def events_summary():
|
||||
has_clip = request.args.get('has_clip', type=int)
|
||||
has_snapshot = request.args.get('has_snapshot', type=int)
|
||||
|
||||
clauses = []
|
||||
|
||||
if not has_clip is None:
|
||||
clauses.append((Event.has_clip == has_clip))
|
||||
|
||||
if not has_snapshot is None:
|
||||
clauses.append((Event.has_snapshot == has_snapshot))
|
||||
|
||||
if len(clauses) == 0:
|
||||
clauses.append((1 == 1))
|
||||
|
||||
groups = (
|
||||
Event
|
||||
.select(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')).alias('day'),
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')).alias('day'),
|
||||
Event.zones,
|
||||
fn.COUNT(Event.id).alias('count')
|
||||
)
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.group_by(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')),
|
||||
Event.zones
|
||||
)
|
||||
@@ -71,8 +89,8 @@ def event(id):
|
||||
except DoesNotExist:
|
||||
return "Event not found", 404
|
||||
|
||||
@bp.route('/events/<id>/snapshot.jpg')
|
||||
def event_snapshot(id):
|
||||
@bp.route('/events/<id>/thumbnail.jpg')
|
||||
def event_thumbnail(id):
|
||||
format = request.args.get('format', 'ios')
|
||||
thumbnail_bytes = None
|
||||
try:
|
||||
@@ -85,25 +103,57 @@ def event_snapshot(id):
|
||||
if id in camera_state.tracked_objects:
|
||||
tracked_obj = camera_state.tracked_objects.get(id)
|
||||
if not tracked_obj is None:
|
||||
thumbnail_bytes = tracked_obj.get_jpg_bytes()
|
||||
thumbnail_bytes = tracked_obj.get_thumbnail()
|
||||
except:
|
||||
return "Event not found", 404
|
||||
|
||||
|
||||
if thumbnail_bytes is None:
|
||||
return "Event not found", 404
|
||||
|
||||
|
||||
# android notifications prefer a 2:1 ratio
|
||||
if format == 'android':
|
||||
jpg_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
|
||||
img = cv2.imdecode(jpg_as_np, flags=1)
|
||||
thumbnail = cv2.copyMakeBorder(img, 0, 0, int(img.shape[1]*0.5), int(img.shape[1]*0.5), cv2.BORDER_CONSTANT, (0,0,0))
|
||||
ret, jpg = cv2.imencode('.jpg', thumbnail)
|
||||
ret, jpg = cv2.imencode('.jpg', thumbnail)
|
||||
thumbnail_bytes = jpg.tobytes()
|
||||
|
||||
|
||||
response = make_response(thumbnail_bytes)
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
|
||||
@bp.route('/events/<id>/snapshot.jpg')
|
||||
def event_snapshot(id):
|
||||
jpg_bytes = None
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
if not event.has_snapshot:
|
||||
return "Snapshot not available", 404
|
||||
# read snapshot from disk
|
||||
with open(os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), 'rb') as image_file:
|
||||
jpg_bytes = image_file.read()
|
||||
except DoesNotExist:
|
||||
# see if the object is currently being tracked
|
||||
try:
|
||||
for camera_state in current_app.detected_frames_processor.camera_states.values():
|
||||
if id in camera_state.tracked_objects:
|
||||
tracked_obj = camera_state.tracked_objects.get(id)
|
||||
if not tracked_obj is None:
|
||||
jpg_bytes = tracked_obj.get_jpg_bytes(
|
||||
timestamp=request.args.get('timestamp', type=int),
|
||||
bounding_box=request.args.get('bbox', type=int),
|
||||
crop=request.args.get('crop', type=int),
|
||||
height=request.args.get('h', type=int)
|
||||
)
|
||||
except:
|
||||
return "Event not found", 404
|
||||
except:
|
||||
return "Event not found", 404
|
||||
|
||||
response = make_response(jpg_bytes)
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
|
||||
@bp.route('/events')
|
||||
def events():
|
||||
limit = request.args.get('limit', 100)
|
||||
@@ -112,24 +162,32 @@ def events():
|
||||
zone = request.args.get('zone')
|
||||
after = request.args.get('after', type=int)
|
||||
before = request.args.get('before', type=int)
|
||||
has_clip = request.args.get('has_clip', type=int)
|
||||
has_snapshot = request.args.get('has_snapshot', type=int)
|
||||
|
||||
clauses = []
|
||||
|
||||
|
||||
if camera:
|
||||
clauses.append((Event.camera == camera))
|
||||
|
||||
|
||||
if label:
|
||||
clauses.append((Event.label == label))
|
||||
|
||||
|
||||
if zone:
|
||||
clauses.append((Event.zones.cast('text') % f"*\"{zone}\"*"))
|
||||
|
||||
|
||||
if after:
|
||||
clauses.append((Event.start_time >= after))
|
||||
|
||||
|
||||
if before:
|
||||
clauses.append((Event.start_time <= before))
|
||||
|
||||
if not has_clip is None:
|
||||
clauses.append((Event.has_clip == has_clip))
|
||||
|
||||
if not has_snapshot is None:
|
||||
clauses.append((Event.has_snapshot == has_snapshot))
|
||||
|
||||
if len(clauses) == 0:
|
||||
clauses.append((1 == 1))
|
||||
|
||||
@@ -144,33 +202,13 @@ 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
|
||||
stats = {}
|
||||
|
||||
total_detection_fps = 0
|
||||
|
||||
for name, camera_stats in camera_metrics.items():
|
||||
total_detection_fps += camera_stats['detection_fps'].value
|
||||
stats[name] = {
|
||||
'camera_fps': round(camera_stats['camera_fps'].value, 2),
|
||||
'process_fps': round(camera_stats['process_fps'].value, 2),
|
||||
'skipped_fps': round(camera_stats['skipped_fps'].value, 2),
|
||||
'detection_fps': round(camera_stats['detection_fps'].value, 2),
|
||||
'pid': camera_stats['process'].pid,
|
||||
'capture_pid': camera_stats['capture_process'].pid
|
||||
}
|
||||
|
||||
stats['detectors'] = {}
|
||||
for name, detector in current_app.detectors.items():
|
||||
stats['detectors'][name] = {
|
||||
'inference_speed': round(detector.avg_inference_speed.value*1000, 2),
|
||||
'detection_start': detector.detection_start.value,
|
||||
'pid': detector.detect_process.pid
|
||||
}
|
||||
stats['detection_fps'] = round(total_detection_fps, 2)
|
||||
|
||||
stats = stats_snapshot(current_app.stats_tracking)
|
||||
return jsonify(stats)
|
||||
|
||||
@bp.route('/<camera_name>/<label>/best.jpg')
|
||||
@@ -182,12 +220,13 @@ def best(camera_name, label):
|
||||
best_frame = np.zeros((720,1280,3), np.uint8)
|
||||
else:
|
||||
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
|
||||
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])))
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
|
||||
@@ -203,18 +242,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)
|
||||
|
||||
@@ -229,12 +284,12 @@ def latest_frame(camera_name):
|
||||
return response
|
||||
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)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import signal
|
||||
import queue
|
||||
import multiprocessing as mp
|
||||
from logging import handlers
|
||||
from setproctitle import setproctitle
|
||||
|
||||
|
||||
def listener_configurer():
|
||||
@@ -31,6 +32,7 @@ def log_process(log_queue):
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
threading.current_thread().name = f"logger"
|
||||
setproctitle("frigate.logger")
|
||||
listener_configurer()
|
||||
while True:
|
||||
if stop_event.is_set() and log_queue.empty():
|
||||
@@ -72,4 +74,4 @@ class LogPipe(threading.Thread):
|
||||
def close(self):
|
||||
"""Close the write end of the pipe.
|
||||
"""
|
||||
os.close(self.fdWrite)
|
||||
os.close(self.fdWrite)
|
||||
|
||||
@@ -12,3 +12,5 @@ class Event(Model):
|
||||
false_positive = BooleanField()
|
||||
zones = JSONField()
|
||||
thumbnail = TextField()
|
||||
has_clip = BooleanField(default=True)
|
||||
has_snapshot = BooleanField(default=True)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
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, 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
|
||||
self.frame_counter = 0
|
||||
resized_mask = cv2.resize(mask, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
resized_mask = cv2.resize(config.mask, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
self.mask = np.where(resized_mask==[0])
|
||||
|
||||
def detect(self, frame):
|
||||
@@ -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
|
||||
|
||||
105
frigate/mqtt.py
@@ -3,12 +3,81 @@ import threading
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from frigate.config import MqttConfig
|
||||
from frigate.config import FrigateConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_mqtt_client(config: MqttConfig):
|
||||
client = mqtt.Client(client_id=config.client_id)
|
||||
def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
||||
mqtt_config = config.mqtt
|
||||
|
||||
def on_clips_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_clips_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split('/')[-3]
|
||||
|
||||
clips_settings = config.cameras[camera_name].clips
|
||||
|
||||
if payload == 'ON':
|
||||
if not clips_settings.enabled:
|
||||
logger.info(f"Turning on clips for {camera_name} via mqtt")
|
||||
clips_settings._enabled = True
|
||||
elif payload == 'OFF':
|
||||
if clips_settings.enabled:
|
||||
logger.info(f"Turning off clips for {camera_name} via mqtt")
|
||||
clips_settings._enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_snapshots_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_snapshots_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split('/')[-3]
|
||||
|
||||
snapshots_settings = config.cameras[camera_name].snapshots
|
||||
|
||||
if payload == 'ON':
|
||||
if not snapshots_settings.enabled:
|
||||
logger.info(f"Turning on snapshots for {camera_name} via mqtt")
|
||||
snapshots_settings._enabled = True
|
||||
elif payload == 'OFF':
|
||||
if snapshots_settings.enabled:
|
||||
logger.info(f"Turning off snapshots for {camera_name} via mqtt")
|
||||
snapshots_settings._enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_detect_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_detect_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split('/')[-3]
|
||||
|
||||
detect_settings = config.cameras[camera_name].detect
|
||||
|
||||
if payload == 'ON':
|
||||
if not camera_metrics[camera_name]["detection_enabled"].value:
|
||||
logger.info(f"Turning on detection for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["detection_enabled"].value = True
|
||||
detect_settings._enabled = True
|
||||
elif payload == 'OFF':
|
||||
if camera_metrics[camera_name]["detection_enabled"].value:
|
||||
logger.info(f"Turning off detection for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["detection_enabled"].value = False
|
||||
detect_settings._enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
threading.current_thread().name = "mqtt"
|
||||
if rc != 0:
|
||||
@@ -22,15 +91,35 @@ def create_mqtt_client(config: MqttConfig):
|
||||
logger.error("Unable to connect to MQTT: Connection refused. Error code: " + str(rc))
|
||||
|
||||
logger.info("MQTT connected")
|
||||
client.publish(config.topic_prefix+'/available', 'online', retain=True)
|
||||
client.publish(mqtt_config.topic_prefix+'/available', 'online', retain=True)
|
||||
|
||||
client = mqtt.Client(client_id=mqtt_config.client_id)
|
||||
client.on_connect = on_connect
|
||||
client.will_set(config.topic_prefix+'/available', payload='offline', qos=1, retain=True)
|
||||
if not config.user is None:
|
||||
client.username_pw_set(config.user, password=config.password)
|
||||
client.will_set(mqtt_config.topic_prefix+'/available', payload='offline', qos=1, retain=True)
|
||||
|
||||
# register callbacks
|
||||
for name in config.cameras.keys():
|
||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/clips/set", on_clips_command)
|
||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/snapshots/set", on_snapshots_command)
|
||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command)
|
||||
|
||||
if not mqtt_config.user is None:
|
||||
client.username_pw_set(mqtt_config.user, password=mqtt_config.password)
|
||||
try:
|
||||
client.connect(config.host, config.port, 60)
|
||||
client.connect(mqtt_config.host, mqtt_config.port, 60)
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to connect to MQTT server: {e}")
|
||||
raise
|
||||
|
||||
client.loop_start()
|
||||
|
||||
for name in config.cameras.keys():
|
||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/clips/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
|
||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/snapshots/state", 'ON' if config.cameras[name].snapshots.enabled else 'OFF', retain=True)
|
||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/detect/state", 'ON' if config.cameras[name].detect.enabled else 'OFF', retain=True)
|
||||
|
||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/clips/set")
|
||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/snapshots/set")
|
||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/detect/set")
|
||||
|
||||
return client
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -54,11 +54,11 @@ def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool:
|
||||
# if the score is better by more than 5%
|
||||
if new_obj['score'] > current_thumb['score']+.05:
|
||||
return True
|
||||
|
||||
|
||||
# if the area is 10% larger
|
||||
if new_obj['area'] > current_thumb['area']*1.1:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
class TrackedObject():
|
||||
@@ -72,11 +72,10 @@ class TrackedObject():
|
||||
self.false_positive = True
|
||||
self.top_score = self.computed_score = 0.0
|
||||
self.thumbnail_data = None
|
||||
self.last_updated = 0
|
||||
self.last_published = 0
|
||||
self.frame = None
|
||||
self.previous = None
|
||||
self._snapshot_jpg_time = 0
|
||||
ret, jpg = cv2.imencode('.jpg', np.zeros((300,300,3), np.uint8))
|
||||
self._snapshot_jpg = jpg.tobytes()
|
||||
self.previous = self.to_dict()
|
||||
|
||||
# start the score history
|
||||
self.score_history = [self.obj_data['score']]
|
||||
@@ -97,9 +96,9 @@ class TrackedObject():
|
||||
if len(scores) < 3:
|
||||
scores += [0.0]*(3 - len(scores))
|
||||
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:
|
||||
@@ -119,7 +118,7 @@ class TrackedObject():
|
||||
if not self.false_positive:
|
||||
# determine if this frame is a better thumbnail
|
||||
if (
|
||||
self.thumbnail_data is None
|
||||
self.thumbnail_data is None
|
||||
or is_better_thumbnail(self.thumbnail_data, self.obj_data, self.camera_config.frame_shape)
|
||||
):
|
||||
self.thumbnail_data = {
|
||||
@@ -129,8 +128,8 @@ class TrackedObject():
|
||||
'region': self.obj_data['region'],
|
||||
'score': self.obj_data['score']
|
||||
}
|
||||
self.previous = previous
|
||||
|
||||
significant_update = True
|
||||
|
||||
# check zones
|
||||
current_zones = []
|
||||
bottom_center = (self.obj_data['centroid'][0], self.obj_data['box'][3])
|
||||
@@ -143,9 +142,14 @@ 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 {
|
||||
'id': self.obj_data['id'],
|
||||
@@ -162,53 +166,62 @@ class TrackedObject():
|
||||
'region': self.obj_data['region'],
|
||||
'current_zones': self.current_zones.copy(),
|
||||
'entered_zones': list(self.entered_zones).copy(),
|
||||
'thumbnail': base64.b64encode(self.get_jpg_bytes()).decode('utf-8') if include_thumbnail else None
|
||||
'thumbnail': base64.b64encode(self.get_thumbnail()).decode('utf-8') if include_thumbnail else None
|
||||
}
|
||||
|
||||
def get_thumbnail(self):
|
||||
if self.thumbnail_data is None or not self.thumbnail_data['frame_time'] in self.frame_cache:
|
||||
ret, jpg = cv2.imencode('.jpg', np.zeros((175,175,3), np.uint8))
|
||||
|
||||
jpg_bytes = self.get_jpg_bytes(timestamp=False, bounding_box=False, crop=True, height=175)
|
||||
|
||||
if jpg_bytes:
|
||||
return jpg_bytes
|
||||
else:
|
||||
ret, jpg = cv2.imencode('.jpg', np.zeros((175,175,3), np.uint8))
|
||||
return jpg.tobytes()
|
||||
|
||||
def get_jpg_bytes(self):
|
||||
if self.thumbnail_data is None or self._snapshot_jpg_time == self.thumbnail_data['frame_time']:
|
||||
return self._snapshot_jpg
|
||||
|
||||
if not self.thumbnail_data['frame_time'] in self.frame_cache:
|
||||
logger.error(f"Unable to create thumbnail for {self.obj_data['id']}")
|
||||
logger.error(f"Looking for frame_time of {self.thumbnail_data['frame_time']}")
|
||||
logger.error(f"Thumbnail frames: {','.join([str(k) for k in self.frame_cache.keys()])}")
|
||||
return self._snapshot_jpg
|
||||
|
||||
# TODO: crop first to avoid converting the entire frame?
|
||||
snapshot_config = self.camera_config.snapshots
|
||||
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
if snapshot_config.draw_bounding_boxes:
|
||||
def get_jpg_bytes(self, timestamp=False, bounding_box=False, crop=False, height=None):
|
||||
if self.thumbnail_data is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
|
||||
except KeyError:
|
||||
logger.warning(f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache")
|
||||
return None
|
||||
|
||||
if bounding_box:
|
||||
thickness = 2
|
||||
color = COLOR_MAP[self.obj_data['label']]
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = self.thumbnail_data['box']
|
||||
draw_box_with_label(best_frame, box[0], box[1], box[2], box[3], self.obj_data['label'],
|
||||
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']
|
||||
draw_box_with_label(best_frame, box[0], box[1], box[2], box[3], self.obj_data['label'], f"{int(self.thumbnail_data['score']*100)}% {int(self.thumbnail_data['area'])}", thickness=thickness, color=color)
|
||||
|
||||
if crop:
|
||||
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:
|
||||
height = snapshot_config.height
|
||||
if height:
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
|
||||
if snapshot_config.show_timestamp:
|
||||
|
||||
if timestamp:
|
||||
time_to_show = datetime.datetime.fromtimestamp(self.thumbnail_data['frame_time']).strftime("%m/%d/%Y %H:%M:%S")
|
||||
size = cv2.getTextSize(time_to_show, cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=2)
|
||||
text_width = size[0][0]
|
||||
desired_size = max(150, 0.33*best_frame.shape[1])
|
||||
font_scale = desired_size/text_width
|
||||
cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=font_scale, color=(255, 255, 255), thickness=2)
|
||||
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
self._snapshot_jpg = jpg.tobytes()
|
||||
|
||||
return self._snapshot_jpg
|
||||
return jpg.tobytes()
|
||||
else:
|
||||
return None
|
||||
|
||||
def zone_filtered(obj: TrackedObject, object_config):
|
||||
object_name = obj.obj_data['label']
|
||||
@@ -220,7 +233,7 @@ def zone_filtered(obj: TrackedObject, object_config):
|
||||
# detected object, don't add it to detected objects
|
||||
if obj_settings.min_area > obj.obj_data['area']:
|
||||
return True
|
||||
|
||||
|
||||
# if the detected object is larger than the
|
||||
# max area, don't add it to detected objects
|
||||
if obj_settings.max_area < obj.obj_data['area']:
|
||||
@@ -229,7 +242,7 @@ def zone_filtered(obj: TrackedObject, object_config):
|
||||
# if the score is lower than the threshold, skip
|
||||
if obj_settings.threshold > obj.computed_score:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
# Maintains the state of a camera
|
||||
@@ -247,23 +260,27 @@ class CameraState():
|
||||
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
|
||||
self.current_frame_lock = threading.Lock()
|
||||
self.current_frame_time = 0.0
|
||||
self.motion_boxes = []
|
||||
self.regions = []
|
||||
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
|
||||
color = COLOR_MAP[obj['label']]
|
||||
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
@@ -271,19 +288,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('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.motion.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)
|
||||
|
||||
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)
|
||||
|
||||
return frame_copy
|
||||
|
||||
def finished(self, obj_id):
|
||||
@@ -292,8 +318,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)
|
||||
@@ -310,20 +338,26 @@ class CameraState():
|
||||
# call event handlers
|
||||
for c in self.callbacks['start']:
|
||||
c(self.name, new_obj, frame_time)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
updated_obj.last_updated = frame_time
|
||||
|
||||
# if it has been more than 5 seconds since the last publish
|
||||
# and the last update is greater than the last publish
|
||||
if frame_time - updated_obj.last_published > 5 and updated_obj.last_updated > updated_obj.last_published:
|
||||
# call event handlers
|
||||
for c in self.callbacks['update']:
|
||||
c(self.name, updated_obj, frame_time)
|
||||
updated_obj.last_published = 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
|
||||
removed_obj = self.tracked_objects[id]
|
||||
@@ -342,9 +376,9 @@ class CameraState():
|
||||
if object_type in self.best_objects:
|
||||
current_best = self.best_objects[object_type]
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# if the object is a higher score than the current best score
|
||||
# if the object is a higher score than the current best score
|
||||
# or the current object is older than desired, use the new object
|
||||
if (is_better_thumbnail(current_best.thumbnail_data, obj.thumbnail_data, self.camera_config.frame_shape)
|
||||
if (is_better_thumbnail(current_best.thumbnail_data, obj.thumbnail_data, self.camera_config.frame_shape)
|
||||
or (now - current_best.thumbnail_data['frame_time']) > self.camera_config.best_image_timeout):
|
||||
self.best_objects[object_type] = obj
|
||||
for c in self.callbacks['snapshot']:
|
||||
@@ -353,13 +387,13 @@ class CameraState():
|
||||
self.best_objects[object_type] = obj
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[object_type], frame_time)
|
||||
|
||||
|
||||
# update overall camera state for each object type
|
||||
obj_counter = Counter()
|
||||
for obj in self.tracked_objects.values():
|
||||
if not obj.false_positive:
|
||||
obj_counter[obj.obj_data['label']] += 1
|
||||
|
||||
|
||||
# report on detected objects
|
||||
for obj_name, count in obj_counter.items():
|
||||
if count != self.object_counts[obj_name]:
|
||||
@@ -375,14 +409,14 @@ class CameraState():
|
||||
c(self.name, obj_name, 0)
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[obj_name], frame_time)
|
||||
|
||||
|
||||
# cleanup thumbnail frame cache
|
||||
current_thumb_frames = set([obj.thumbnail_data['frame_time'] for obj in self.tracked_objects.values() if not obj.false_positive])
|
||||
current_best_frames = set([obj.thumbnail_data['frame_time'] for obj in self.best_objects.values()])
|
||||
thumb_frames_to_delete = [t for t in self.frame_cache.keys() if not t in current_thumb_frames and not t in current_best_frames]
|
||||
for t in thumb_frames_to_delete:
|
||||
del self.frame_cache[t]
|
||||
|
||||
|
||||
with self.current_frame_lock:
|
||||
self._current_frame = current_frame
|
||||
if not self.previous_frame_id is None:
|
||||
@@ -407,18 +441,41 @@ 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, 'type': 'new' if obj.previous['false_positive'] else 'update' }
|
||||
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
|
||||
obj.previous = after
|
||||
|
||||
def end(camera, obj: TrackedObject, current_frame_time):
|
||||
snapshot_config = self.config.cameras[camera].snapshots
|
||||
event_data = obj.to_dict(include_thumbnail=True)
|
||||
event_data['has_snapshot'] = False
|
||||
if not obj.false_positive:
|
||||
message = { 'before': obj.previous, 'after': obj.to_dict() }
|
||||
message = { 'before': obj.previous, 'after': obj.to_dict(), 'type': 'end' }
|
||||
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
|
||||
self.event_queue.put(('end', camera, obj.to_dict(include_thumbnail=True)))
|
||||
# write snapshot to disk if enabled
|
||||
if snapshot_config.enabled:
|
||||
jpg_bytes = obj.get_jpg_bytes(
|
||||
timestamp=snapshot_config.timestamp,
|
||||
bounding_box=snapshot_config.bounding_box,
|
||||
crop=snapshot_config.crop,
|
||||
height=snapshot_config.height
|
||||
)
|
||||
with open(os.path.join(CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"), 'wb') as j:
|
||||
j.write(jpg_bytes)
|
||||
event_data['has_snapshot'] = True
|
||||
self.event_queue.put(('end', camera, event_data))
|
||||
|
||||
def snapshot(camera, obj: TrackedObject, current_frame_time):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot", obj.get_jpg_bytes(), retain=True)
|
||||
mqtt_config = self.config.cameras[camera].mqtt
|
||||
if mqtt_config.enabled:
|
||||
jpg_bytes = obj.get_jpg_bytes(
|
||||
timestamp=mqtt_config.timestamp,
|
||||
bounding_box=mqtt_config.bounding_box,
|
||||
crop=mqtt_config.crop,
|
||||
height=mqtt_config.height
|
||||
)
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot", jpg_bytes, retain=True)
|
||||
|
||||
def object_status(camera, object_name, status):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
|
||||
@@ -441,20 +498,20 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
# }
|
||||
# }
|
||||
self.zone_data = defaultdict(lambda: defaultdict(lambda: {}))
|
||||
|
||||
|
||||
def get_best(self, camera, label):
|
||||
# TODO: need a lock here
|
||||
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 +520,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
|
||||
@@ -479,7 +536,7 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
for obj in camera_state.tracked_objects.values():
|
||||
if zone in obj.current_zones and not obj.false_positive:
|
||||
obj_counter[obj.obj_data['label']] += 1
|
||||
|
||||
|
||||
# update counts and publish status
|
||||
for label in set(list(self.zone_data[zone].keys()) + list(obj_counter.keys())):
|
||||
# if we have previously published a count for this zone/label
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -45,9 +45,9 @@ class RecordingMaintainer(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:
|
||||
@@ -98,9 +98,9 @@ class RecordingMaintainer(threading.Thread):
|
||||
delete_before[name] = datetime.datetime.now().timestamp() - SECONDS_IN_DAY*camera.record.retain_days
|
||||
|
||||
for p in Path('/media/frigate/recordings').rglob("*.mp4"):
|
||||
if not p.parent in delete_before:
|
||||
if not p.parent.name in delete_before:
|
||||
continue
|
||||
if p.stat().st_mtime < delete_before[p.parent]:
|
||||
if p.stat().st_mtime < delete_before[p.parent.name]:
|
||||
p.unlink(missing_ok=True)
|
||||
|
||||
def run(self):
|
||||
@@ -122,4 +122,4 @@ class RecordingMaintainer(threading.Thread):
|
||||
self.move_files()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
70
frigate/stats.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.version import VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def stats_init(camera_metrics, detectors):
|
||||
stats_tracking = {
|
||||
'camera_metrics': camera_metrics,
|
||||
'detectors': detectors,
|
||||
'started': int(time.time())
|
||||
}
|
||||
return stats_tracking
|
||||
|
||||
def stats_snapshot(stats_tracking):
|
||||
camera_metrics = stats_tracking['camera_metrics']
|
||||
stats = {}
|
||||
|
||||
total_detection_fps = 0
|
||||
|
||||
for name, camera_stats in camera_metrics.items():
|
||||
total_detection_fps += camera_stats['detection_fps'].value
|
||||
stats[name] = {
|
||||
'camera_fps': round(camera_stats['camera_fps'].value, 2),
|
||||
'process_fps': round(camera_stats['process_fps'].value, 2),
|
||||
'skipped_fps': round(camera_stats['skipped_fps'].value, 2),
|
||||
'detection_fps': round(camera_stats['detection_fps'].value, 2),
|
||||
'pid': camera_stats['process'].pid,
|
||||
'capture_pid': camera_stats['capture_process'].pid
|
||||
}
|
||||
|
||||
stats['detectors'] = {}
|
||||
for name, detector in stats_tracking["detectors"].items():
|
||||
stats['detectors'][name] = {
|
||||
'inference_speed': round(detector.avg_inference_speed.value * 1000, 2),
|
||||
'detection_start': detector.detection_start.value,
|
||||
'pid': detector.detect_process.pid
|
||||
}
|
||||
stats['detection_fps'] = round(total_detection_fps, 2)
|
||||
|
||||
stats['service'] = {
|
||||
'uptime': (int(time.time()) - stats_tracking['started']),
|
||||
'version': VERSION
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
class StatsEmitter(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stats_tracking, mqtt_client, topic_prefix, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = 'frigate_stats_emitter'
|
||||
self.config = config
|
||||
self.stats_tracking = stats_tracking
|
||||
self.mqtt_client = mqtt_client
|
||||
self.topic_prefix = topic_prefix
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
time.sleep(10)
|
||||
while True:
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting watchdog...")
|
||||
break
|
||||
stats = stats_snapshot(self.stats_tracking)
|
||||
self.mqtt_client.publish(f"{self.topic_prefix}/stats", json.dumps(stats), retain=False)
|
||||
time.sleep(self.config.mqtt.stats_interval)
|
||||
@@ -191,12 +191,12 @@ class TestConfig(TestCase):
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('-re' in frigate_config.cameras['back'].ffmpeg_cmds[0]['cmd'])
|
||||
|
||||
def test_inherit_save_clips_retention(self):
|
||||
def test_inherit_clips_retention(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
@@ -217,14 +217,14 @@ class TestConfig(TestCase):
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert(frigate_config.cameras['back'].save_clips.retain.objects['person'] == 30)
|
||||
assert(frigate_config.cameras['back'].clips.retain.objects['person'] == 30)
|
||||
|
||||
def test_roles_listed_twice_throws_error(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
@@ -252,7 +252,7 @@ class TestConfig(TestCase):
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
@@ -279,12 +279,12 @@ class TestConfig(TestCase):
|
||||
}
|
||||
self.assertRaises(vol.MultipleInvalid, lambda: FrigateConfig(config=config))
|
||||
|
||||
def test_save_clips_should_default_to_global_objects(self):
|
||||
def test_clips_should_default_to_global_objects(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
@@ -304,16 +304,39 @@ class TestConfig(TestCase):
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'save_clips': {
|
||||
'clips': {
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config = FrigateConfig(config=config)
|
||||
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)
|
||||
assert(config.cameras['back'].clips.objects is None)
|
||||
|
||||
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/record', 'roles': ['record'] }
|
||||
]
|
||||
},
|
||||
'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)
|
||||
|
||||
39
frigate/test/test_yuv_region_2_rgb.py
Normal 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)
|
||||
188
frigate/util.py
@@ -2,6 +2,7 @@ import collections
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import signal
|
||||
import subprocess as sp
|
||||
import threading
|
||||
@@ -15,6 +16,8 @@ import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'):
|
||||
if color is None:
|
||||
@@ -47,14 +50,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 +62,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 (
|
||||
@@ -183,6 +291,24 @@ def print_stack(sig, frame):
|
||||
def listen():
|
||||
signal.signal(signal.SIGUSR1, print_stack)
|
||||
|
||||
def create_mask(frame_shape, mask):
|
||||
mask_img = np.zeros(frame_shape, np.uint8)
|
||||
mask_img[:] = 255
|
||||
|
||||
if isinstance(mask, list):
|
||||
for m in mask:
|
||||
add_mask(m, mask_img)
|
||||
|
||||
elif isinstance(mask, str):
|
||||
add_mask(mask, mask_img)
|
||||
|
||||
return mask_img
|
||||
|
||||
def add_mask(mask, mask_img):
|
||||
points = mask.split(',')
|
||||
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))
|
||||
|
||||
class FrameManager(ABC):
|
||||
@abstractmethod
|
||||
def create(self, name, size) -> AnyStr:
|
||||
|
||||
@@ -13,6 +13,7 @@ import signal
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from setproctitle import setproctitle
|
||||
from typing import Dict, List
|
||||
|
||||
import cv2
|
||||
@@ -30,7 +31,7 @@ from frigate.util import (EventsPerSecond, FrameManager,
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def filtered(obj, objects_to_track, object_filters, mask=None):
|
||||
def filtered(obj, objects_to_track, object_filters):
|
||||
object_name = obj[0]
|
||||
|
||||
if not object_name in objects_to_track:
|
||||
@@ -53,25 +54,26 @@ def filtered(obj, objects_to_track, object_filters, mask=None):
|
||||
if obj_settings.min_score > obj[1]:
|
||||
return True
|
||||
|
||||
# compute the coordinates of the object and make sure
|
||||
# the location isnt outside the bounds of the image (can happen from rounding)
|
||||
y_location = min(int(obj[2][3]), len(mask)-1)
|
||||
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
|
||||
if not obj_settings.mask is None:
|
||||
# compute the coordinates of the object and make sure
|
||||
# the location isnt outside the bounds of the image (can happen from rounding)
|
||||
y_location = min(int(obj[2][3]), len(obj_settings.mask)-1)
|
||||
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(obj_settings.mask[0])-1)
|
||||
|
||||
# if the object is in a masked location, don't add it to detected objects
|
||||
if (not mask is None) and (mask[y_location][x_location] == 0):
|
||||
return True
|
||||
# if the object is in a masked location, don't add it to detected objects
|
||||
if obj_settings.mask[y_location][x_location] == 0:
|
||||
return True
|
||||
|
||||
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 +114,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 +242,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()
|
||||
@@ -250,24 +251,25 @@ def track_camera(name, config: CameraConfig, detection_queue, result_connection,
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
threading.current_thread().name = f"process:{name}"
|
||||
setproctitle(f"frigate.process:{name}")
|
||||
listen()
|
||||
|
||||
frame_queue = process_info['frame_queue']
|
||||
detection_enabled = process_info['detection_enabled']
|
||||
|
||||
frame_shape = config.frame_shape
|
||||
objects_to_track = config.objects.track
|
||||
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, 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,
|
||||
object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, mask, stop_event)
|
||||
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, detection_enabled, stop_event)
|
||||
|
||||
logger.info(f"{name}: exiting subprocess")
|
||||
|
||||
@@ -277,8 +279,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):
|
||||
tensor_input = create_tensor_input(frame, model_shape, region)
|
||||
|
||||
detections = []
|
||||
region_detections = object_detector.detect(tensor_input)
|
||||
@@ -295,16 +297,16 @@ def detect(object_detector, frame, region, objects_to_track, object_filters, mas
|
||||
(x_max-x_min)*(y_max-y_min),
|
||||
region)
|
||||
# apply object filters
|
||||
if filtered(det, objects_to_track, object_filters, mask):
|
||||
if filtered(det, objects_to_track, object_filters):
|
||||
continue
|
||||
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,
|
||||
objects_to_track: List[str], object_filters, mask, stop_event,
|
||||
objects_to_track: List[str], object_filters, detection_enabled: mp.Value, stop_event,
|
||||
exit_on_empty: bool = False):
|
||||
|
||||
fps = process_info['process_fps']
|
||||
@@ -335,6 +337,14 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
|
||||
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
|
||||
continue
|
||||
|
||||
if not detection_enabled.value:
|
||||
fps.value = fps_tracker.eps()
|
||||
object_tracker.match_and_update(frame_time, [])
|
||||
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects, [], []))
|
||||
detection_fps.value = object_detector.fps.eps()
|
||||
frame_manager.close(f"{camera_name}{frame_time}")
|
||||
continue
|
||||
|
||||
# look for motion
|
||||
motion_boxes = motion_detector.detect(frame)
|
||||
|
||||
@@ -357,7 +367,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))
|
||||
|
||||
#########
|
||||
# merge objects, check for clipped objects and look again up to 4 times
|
||||
@@ -389,8 +399,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))
|
||||
|
||||
refining = True
|
||||
else:
|
||||
@@ -407,11 +419,11 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
|
||||
|
||||
# add to the queue if not full
|
||||
if(detected_objects_queue.full()):
|
||||
frame_manager.delete(f"{camera_name}{frame_time}")
|
||||
continue
|
||||
frame_manager.delete(f"{camera_name}{frame_time}")
|
||||
continue
|
||||
else:
|
||||
fps_tracker.update()
|
||||
fps.value = fps_tracker.eps()
|
||||
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects))
|
||||
detection_fps.value = object_detector.fps.eps()
|
||||
frame_manager.close(f"{camera_name}{frame_time}")
|
||||
fps_tracker.update()
|
||||
fps.value = fps_tracker.eps()
|
||||
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}")
|
||||
|
||||
@@ -2,6 +2,8 @@ import datetime
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import signal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,5 +34,5 @@ class FrigateWatchdog(threading.Thread):
|
||||
logger.info("Detection appears to be stuck. Restarting detection process")
|
||||
detector.start_or_restart()
|
||||
elif not detector.detect_process.is_alive():
|
||||
logger.info("Detection appears to have stopped. Restarting detection process")
|
||||
detector.start_or_restart()
|
||||
logger.info("Detection appears to have stopped. Restarting frigate")
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
41
migrations/001_create_events_table.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Peewee migrations -- 001_create_events_table.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.python(func, *args, **kwargs) # Run python code
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
import peewee as pw
|
||||
from decimal import ROUND_HALF_EVEN
|
||||
|
||||
try:
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
SQL = pw.SQL
|
||||
|
||||
def migrate(migrator, database, fake=False, **kwargs):
|
||||
migrator.sql('CREATE TABLE IF NOT EXISTS "event" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "label" VARCHAR(20) NOT NULL, "camera" VARCHAR(20) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "top_score" REAL NOT NULL, "false_positive" INTEGER NOT NULL, "zones" JSON NOT NULL, "thumbnail" TEXT NOT NULL)')
|
||||
migrator.sql('CREATE INDEX IF NOT EXISTS "event_label" ON "event" ("label")')
|
||||
migrator.sql('CREATE INDEX IF NOT EXISTS "event_camera" ON "event" ("camera")')
|
||||
|
||||
def rollback(migrator, database, fake=False, **kwargs):
|
||||
pass
|
||||
41
migrations/002_add_clip_snapshot.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Peewee migrations -- 002_add_clip_snapshot.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.python(func, *args, **kwargs) # Run python code
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
import peewee as pw
|
||||
from decimal import ROUND_HALF_EVEN
|
||||
from frigate.models import Event
|
||||
|
||||
try:
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
SQL = pw.SQL
|
||||
|
||||
|
||||
def migrate(migrator, database, fake=False, **kwargs):
|
||||
migrator.add_fields(Event, has_clip=pw.BooleanField(default=True), has_snapshot=pw.BooleanField(default=True))
|
||||
|
||||
def rollback(migrator, database, fake=False, **kwargs):
|
||||
migrator.remove_fields(Event, ['has_clip', 'has_snapshot'])
|
||||
@@ -96,13 +96,25 @@ http {
|
||||
root /media/frigate;
|
||||
}
|
||||
|
||||
location / {
|
||||
location /api/ {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
proxy_pass http://frigate_api/;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
||||
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
||||
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
||||
sub_filter_types text/css application/javascript;
|
||||
sub_filter_once off;
|
||||
root /opt/frigate/web;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,4 +131,4 @@ rtmp {
|
||||
meta copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
152
process_clip.py
@@ -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()
|
||||
1
web/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
8
web/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Frigate Web UI
|
||||
|
||||
## Development
|
||||
|
||||
1. Build the docker images in the root of the repository `make amd64_all` (or appropriate for your system)
|
||||
2. Create a config file in `config/`
|
||||
3. Run the container: `docker run --rm --name frigate --privileged -v $PWD/config:/config:ro -v /etc/localtime:/etc/localtime:ro -p 5000:5000 frigate`
|
||||
4. Run the dev ui: `cd web && npm run start`
|
||||
8497
web/package-lock.json
generated
Normal file
24
web/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "frigate",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "cross-env SNOWPACK_PUBLIC_API_HOST=http://localhost:5000 snowpack dev",
|
||||
"prebuild": "rimraf build",
|
||||
"build": "snowpack build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prefresh/snowpack": "^3.0.1",
|
||||
"@snowpack/plugin-optimize": "^0.2.13",
|
||||
"@snowpack/plugin-postcss": "^1.1.0",
|
||||
"@snowpack/plugin-webpack": "^2.3.0",
|
||||
"autoprefixer": "^10.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"postcss": "^8.2.2",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"preact": "^10.5.9",
|
||||
"preact-router": "^3.2.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"snowpack": "^3.0.0",
|
||||
"tailwindcss": "^2.0.2"
|
||||
}
|
||||
}
|
||||
8
web/postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
||||
BIN
web/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
web/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
web/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
web/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
web/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 800 B |
BIN
web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
web/public/favicon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
21
web/public/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>Frigate</title>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3b82f7" />
|
||||
<meta name="msapplication-TileColor" content="#3b82f7" />
|
||||
<meta name="theme-color" content="#ff0000" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
46
web/public/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="888.000000pt" height="888.000000pt" viewBox="0 0 888.000000 888.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,888.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M8228 8865 c-2 -2 -25 -6 -53 -9 -38 -5 -278 -56 -425 -91 -33 -7
|
||||
-381 -98 -465 -121 -49 -14 -124 -34 -165 -45 -67 -18 -485 -138 -615 -176
|
||||
-50 -14 -106 -30 -135 -37 -8 -2 -35 -11 -60 -19 -25 -8 -85 -27 -135 -42 -49
|
||||
-14 -101 -31 -115 -36 -14 -5 -34 -11 -45 -13 -11 -3 -65 -19 -120 -36 -55
|
||||
-18 -127 -40 -160 -50 -175 -53 -247 -77 -550 -178 -364 -121 -578 -200 -820
|
||||
-299 -88 -36 -214 -88 -280 -115 -66 -27 -129 -53 -140 -58 -11 -5 -67 -29
|
||||
-125 -54 -342 -144 -535 -259 -579 -343 -34 -66 7 -145 156 -299 229 -238 293
|
||||
-316 340 -413 38 -80 41 -152 10 -281 -57 -234 -175 -543 -281 -732 -98 -174
|
||||
-172 -239 -341 -297 -116 -40 -147 -52 -210 -80 -107 -49 -179 -107 -290 -236
|
||||
-51 -59 -179 -105 -365 -131 -19 -2 -48 -7 -65 -9 -16 -3 -50 -8 -75 -11 -69
|
||||
-9 -130 -39 -130 -63 0 -24 31 -46 78 -56 18 -4 139 -8 270 -10 250 -4 302
|
||||
-11 335 -44 19 -18 19 -23 7 -46 -19 -36 -198 -121 -490 -233 -850 -328 -914
|
||||
-354 -1159 -473 -185 -90 -337 -186 -395 -249 -60 -65 -67 -107 -62 -350 3
|
||||
-113 7 -216 10 -230 3 -14 7 -52 10 -85 7 -70 14 -128 21 -170 2 -16 7 -48 10
|
||||
-70 3 -22 11 -64 16 -94 6 -30 12 -64 14 -75 1 -12 5 -34 9 -51 3 -16 8 -39
|
||||
10 -50 12 -57 58 -258 71 -310 9 -33 18 -69 20 -79 25 -110 138 -416 216 -582
|
||||
21 -47 39 -87 39 -90 0 -7 217 -438 261 -521 109 -201 293 -501 347 -564 11
|
||||
-13 37 -44 56 -68 69 -82 126 -109 160 -75 26 25 14 65 -48 164 -138 218 -142
|
||||
245 -138 800 2 206 4 488 5 625 1 138 -1 293 -6 345 -28 345 -28 594 -1 760
|
||||
12 69 54 187 86 235 33 52 188 212 293 302 98 84 108 93 144 121 19 15 52 42
|
||||
75 61 78 64 302 229 426 313 248 169 483 297 600 326 53 14 205 6 365 -17 33
|
||||
-5 155 -8 270 -6 179 3 226 7 316 28 58 13 140 25 182 26 82 2 120 6 217 22
|
||||
73 12 97 16 122 18 12 1 23 21 38 70 l20 68 74 -17 c81 -20 155 -30 331 -45
|
||||
69 -6 132 -8 715 -20 484 -11 620 -8 729 16 85 19 131 63 98 96 -25 26 -104
|
||||
34 -302 32 -373 -2 -408 -1 -471 26 -90 37 2 102 171 120 33 3 76 8 95 10 19
|
||||
2 71 7 115 10 243 17 267 20 338 37 145 36 47 102 -203 137 -136 19 -262 25
|
||||
-490 22 -124 -2 -362 -4 -530 -4 l-305 -1 -56 26 c-65 31 -171 109 -238 176
|
||||
-52 51 -141 173 -141 191 0 6 -6 22 -14 34 -18 27 -54 165 -64 244 -12 98 -6
|
||||
322 12 414 9 47 29 127 45 176 26 80 58 218 66 278 1 11 6 47 10 80 3 33 8 70
|
||||
10 83 2 13 7 53 11 90 3 37 8 74 9 83 22 118 22 279 -1 464 -20 172 -20 172
|
||||
70 238 108 79 426 248 666 355 25 11 77 34 115 52 92 42 443 191 570 242 55
|
||||
22 109 44 120 48 24 11 130 52 390 150 199 75 449 173 500 195 17 7 118 50
|
||||
225 95 237 100 333 143 490 220 229 113 348 191 337 223 -3 10 -70 20 -79 12z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
19
web/public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ff0000",
|
||||
"background_color": "#ff0000",
|
||||
"display": "standalone"
|
||||
}
|
||||
31
web/snowpack.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
mount: {
|
||||
public: { url: '/', static: true },
|
||||
src: { url: '/dist' },
|
||||
},
|
||||
plugins: [
|
||||
'@snowpack/plugin-postcss',
|
||||
'@prefresh/snowpack',
|
||||
[
|
||||
'@snowpack/plugin-optimize',
|
||||
{
|
||||
preloadModules: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@snowpack/plugin-webpack',
|
||||
{
|
||||
sourceMap: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
|
||||
packageOptions: {
|
||||
sourcemap: false,
|
||||
},
|
||||
buildOptions: {
|
||||
sourcemap: true,
|
||||
},
|
||||
};
|
||||
43
web/src/App.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { h } from 'preact';
|
||||
import Camera from './Camera';
|
||||
import CameraMap from './CameraMap';
|
||||
import Cameras from './Cameras';
|
||||
import Debug from './Debug';
|
||||
import Event from './Event';
|
||||
import Events from './Events';
|
||||
import { Router } from 'preact-router';
|
||||
import Sidebar from './Sidebar';
|
||||
import { ApiHost, Config } from './context';
|
||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function App() {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(async () => {
|
||||
const response = await fetch(`${apiHost}/api/config`);
|
||||
const data = response.ok ? await response.json() : {};
|
||||
setConfig(data);
|
||||
}, []);
|
||||
|
||||
return !config ? (
|
||||
<div />
|
||||
) : (
|
||||
<Config.Provider value={config}>
|
||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
<Sidebar />
|
||||
<div className="p-4 min-w-0">
|
||||
<Router>
|
||||
<CameraMap path="/cameras/:camera/editor" />
|
||||
<Camera path="/cameras/:camera" />
|
||||
<Event path="/events/:eventId" />
|
||||
<Events path="/events" />
|
||||
<Debug path="/debug" />
|
||||
<Cameras default path="/" />
|
||||
</Router>
|
||||
</div>
|
||||
</div>
|
||||
</Config.Provider>
|
||||
);
|
||||
return;
|
||||
}
|
||||
68
web/src/Camera.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { h } from 'preact';
|
||||
import AutoUpdatingCameraImage from './components/AutoUpdatingCameraImage';
|
||||
import Box from './components/Box';
|
||||
import Heading from './components/Heading';
|
||||
import Link from './components/Link';
|
||||
import Switch from './components/Switch';
|
||||
import { route } from 'preact-router';
|
||||
import { useCallback, useContext } from 'preact/hooks';
|
||||
import { ApiHost, Config } from './context';
|
||||
|
||||
export default function Camera({ camera, url }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
|
||||
if (!(camera in config.cameras)) {
|
||||
return <div>{`No camera named ${camera}`}</div>;
|
||||
}
|
||||
|
||||
const cameraConfig = config.cameras[camera];
|
||||
|
||||
const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url}`);
|
||||
const searchParamsString = searchParams.toString();
|
||||
|
||||
const handleSetOption = useCallback(
|
||||
(id, value) => {
|
||||
searchParams.set(id, value ? 1 : 0);
|
||||
route(`${pathname}?${searchParams.toString()}`, true);
|
||||
},
|
||||
[searchParams]
|
||||
);
|
||||
|
||||
function getBoolean(id) {
|
||||
return Boolean(parseInt(searchParams.get(id), 10));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Heading size="2xl">{camera}</Heading>
|
||||
<Box>
|
||||
<AutoUpdatingCameraImage camera={camera} searchParams={searchParamsString} />
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||
<Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} />
|
||||
<Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} />
|
||||
<Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} />
|
||||
<Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} />
|
||||
<Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
|
||||
<Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
|
||||
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
||||
</Box>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Heading size="sm">Tracked objects</Heading>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-4">
|
||||
{cameraConfig.objects.track.map((objectType) => {
|
||||
return (
|
||||
<Box key={objectType} hover href={`/events?camera=${camera}&label=${objectType}`}>
|
||||
<Heading size="sm">{objectType}</Heading>
|
||||
<img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||