Compare commits

...

72 Commits

Author SHA1 Message Date
Blake Blackshear
e6d2df5661 add duration to cache 2021-11-19 16:56:00 -06:00
Blake Blackshear
a3301e0347 avoid running ffprobe for each segment multiple times 2021-11-19 07:28:51 -06:00
Blake Blackshear
3d556cc2cb warn if no wait time 2021-11-19 07:19:14 -06:00
Blake Blackshear
585efe1a0f keep 5 segments in cache 2021-11-19 07:16:29 -06:00
Blake Blackshear
c7d47439dd better cache handling 2021-11-17 08:57:57 -06:00
Blake Blackshear
19a6978228 avoid proactive messages with retain_days 0 and handle first pass 2021-11-17 07:44:58 -06:00
Blake Blackshear
1ebb8a54bf avoid divide by zero 2021-11-17 07:29:23 -06:00
Blake Blackshear
ae968044d6 revert switch to b/w frame prep 2021-11-17 07:28:53 -06:00
Blake Blackshear
b912851e49 fix default motion comment 2021-11-15 06:54:03 -06:00
Blake Blackshear
14c74e4361 more robust cache management 2021-11-10 21:12:41 -06:00
Blake Blackshear
51fb532e1a set retain when setting switches from frontend 2021-11-09 07:40:23 -06:00
Blake Blackshear
3541f966e3 error handling for the recording maintainer 2021-11-09 07:05:21 -06:00
Blake Blackshear
c7faef8faa don't modify ffmpeg_cmd object 2021-11-08 19:05:39 -06:00
Blake Blackshear
cdd3000315 fix ffmpeg config for env vars 2021-11-08 18:20:47 -06:00
Blake Blackshear
1c1c28d0e5 create ffmpeg commands on startup 2021-11-08 07:36:21 -06:00
Blake Blackshear
4422e86907 clarify shm in docs 2021-11-08 07:36:21 -06:00
Blake Blackshear
8f43a2d109 use resolution of clip 2021-11-08 07:36:21 -06:00
Blake Blackshear
bd7755fdd3 revamp process clip 2021-11-08 07:36:21 -06:00
Blake Blackshear
d554175631 no longer make motion settings dynamic 2021-11-08 07:36:21 -06:00
Blake Blackshear
ff667b019a remove min frame height of 180 and increase contour area 2021-11-08 07:36:21 -06:00
Blake Blackshear
57dcb29f8b consolidate regions 2021-11-08 07:36:21 -06:00
Blake Blackshear
9dc6c423b7 improve contrast 2021-11-08 07:36:21 -06:00
Blake Blackshear
58117e2a3e check for overlapping motion boxes 2021-11-08 07:36:21 -06:00
Blake Blackshear
5bec438f9c config option for stationary detection interval 2021-11-01 07:58:30 -05:00
Blake Blackshear
24cc63d6d3 drop high overlap detections 2021-11-01 07:58:30 -05:00
Blake Blackshear
d17bd74c9a reduce detection rate for stationary objects 2021-11-01 07:58:30 -05:00
Blake Blackshear
8f101ccca8 improve box merging and keep tracking 2021-11-01 07:58:30 -05:00
Blake Blackshear
b63c56d810 only save recordings when an event is in progress 2021-10-25 06:40:36 -05:00
Blake Blackshear
61c62d4685 version tick 2021-10-25 06:40:02 -05:00
Blake Blackshear
26ae6084ea fix rtmp again 2021-10-24 13:53:43 -05:00
Blake Blackshear
76142e9699 version tick 2021-10-24 13:53:43 -05:00
Blake Blackshear
5e692acfbb add links in docs to other sites 2021-10-23 09:41:32 -05:00
Blake Blackshear
a67b8ab84d validate with runtime config (fixes #2055) 2021-10-23 08:21:15 -05:00
Blake Blackshear
4cf55ad8e2 Revert switch to mpegts format and audio default 2021-10-23 08:21:15 -05:00
Blake Blackshear
c1132e6897 update ignore files 2021-10-23 08:21:15 -05:00
Blake Blackshear
d6104f2eb2 add storage info to docs 2021-10-23 08:21:15 -05:00
Blake Blackshear
b0e0abe385 improve performance of cache loop 2021-10-23 08:21:15 -05:00
Blake Blackshear
4916e1cd1d hide banner for ffmpeg conversion 2021-10-23 08:21:15 -05:00
Blake Blackshear
cd87f3e6f4 fix old style recording cleanup 2021-10-23 08:21:15 -05:00
Blake Blackshear
18f4ab2644 version tick 2021-10-23 08:21:15 -05:00
Lindsay Ward
0bd3be94ec Clarify environment variables
Based on issue #1976 - specify explicitly that these fields can include environment variables to avoid interpretation that environment variables could be used anywhere.
I am participating in #hacktoberfest, so I would appreciate if you could add the 'hacktoberfest-accepted' label (or add #hacktoberfest topic to your repo). Thanks!
2021-10-23 06:42:53 -05:00
Blake Blackshear
25bb515afc Merge pull request #2026 from blakeblackshear/recording_fix
0.9.2
2021-10-19 20:43:25 -05:00
Blake Blackshear
7ab6961ee1 use live dimensions 2021-10-17 08:48:59 -05:00
Blake Blackshear
ae24cf3bb2 set max width/height for live view 2021-10-17 07:48:56 -05:00
Blake Blackshear
2e494477a6 backwards compatibility for segment_type 2021-10-16 10:36:13 -05:00
Blake Blackshear
80b72c75d9 revert jest update 2021-10-16 08:12:22 -05:00
Blake Blackshear
9494bb7f5f frontend dependency updates 2021-10-16 07:57:59 -05:00
Blake Blackshear
86a741b6e6 assign roles when single input and consolidate validation 2021-10-16 07:46:39 -05:00
Blake Blackshear
f738275d21 yell about config validation errors
for the people in the back
2021-10-16 07:17:36 -05:00
Blake Blackshear
e297e02800 store audio by default 2021-10-16 06:06:49 -05:00
Blake Blackshear
b2e05afff2 prevent oldest recording from being deleted 2021-10-15 21:56:03 -05:00
Blake Blackshear
05fc35fc3d update hardware docs 2021-10-15 21:29:36 -05:00
Blake Blackshear
c809494c98 switch to mpegts format for cache and create mp4 with faststart 2021-10-15 21:08:43 -05:00
Blake Blackshear
ef82c5c691 fix expiration when event spans the exire date 2021-10-15 07:30:55 -05:00
Blake Blackshear
c0e2a75715 version tick 2021-10-15 07:30:35 -05:00
Blake Blackshear
01ddd00bc5 Merge pull request #1975 from blakeblackshear/hassos_docs
update hassos warning
2021-10-10 07:39:11 -05:00
Blake Blackshear
d150f01a2c update hassos warning 2021-10-10 07:32:55 -05:00
Blake Blackshear
f9e159deaf Merge pull request #1968 from FM-17/patch-1
warning for dev board incompatibility post-0.9.x
2021-10-09 11:57:46 -05:00
FM-17
381b00157e warning for dev board incompatibility post-0.9.x
Hoped to investigate this with my dev board at some point. In the meantime, added a warning for others who may experience it when upgrading to the new stable release.
2021-10-09 11:23:51 -03:00
Blake Blackshear
800f33e7be version tick 2021-10-05 19:02:38 -05:00
Blake Blackshear
b8218876be Merge pull request #1922 from blakeblackshear/fix_logo
fix logo used for birdseye
2021-10-05 18:57:07 -05:00
Blake Blackshear
5669f4c161 fix logo used for birdseye 2021-10-05 18:40:46 -05:00
Blake Blackshear
c492b30adb Merge pull request #825 from blakeblackshear/release-0.9.0
Release 0.9.0
2021-10-05 17:59:25 -05:00
Kevin Pelzel
eb48722126 added white background to apple-touch-icon 2021-10-05 17:37:18 -05:00
Blake Blackshear
8e881b60f0 update hardware recommendations 2021-10-05 07:13:13 -05:00
Blake Blackshear
0260d824a6 further doc clarifications 2021-10-05 06:57:17 -05:00
Blake Blackshear
0877a7dec7 Create config.yml 2021-10-04 17:20:58 -05:00
Blake Blackshear
4c7919ad69 updated links 2021-10-04 08:54:35 -05:00
Blake Blackshear
4e997124b3 update latest recommendations for reolink 2021-10-04 07:18:53 -05:00
Blake Blackshear
8b040f5c95 optimize images for web 2021-10-04 07:00:30 -05:00
Blake Blackshear
96156805ed Delete bug_report.md 2021-10-03 08:53:19 -05:00
Blake Blackshear
b8d48d7e62 Create support_request.yml 2021-10-03 08:51:53 -05:00
55 changed files with 1303 additions and 846 deletions

View File

@@ -7,4 +7,5 @@ config/
.git .git
core core
*.mp4 *.mp4
*.db *.db
*.ts

View File

@@ -1,56 +0,0 @@
---
name: Bug report or Support request
about: Bug report or Support request
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what your issue is.
**Version of frigate**
Output from `/api/version`
**Config file**
Include your full config file wrapped in triple back ticks.
```yaml
config here
```
**Frigate container logs**
```
Include relevant log output here
```
**Frigate stats**
```json
Output from frigate's /api/stats endpoint
```
**FFprobe from your camera**
Run the following command and paste output below
```
ffprobe <stream_url>
```
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Computer Hardware**
- OS: [e.g. Ubuntu, Windows]
- 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]
**Camera Info:**
- Manufacturer: [e.g. Dahua]
- Model: [e.g. IPC-HDW5231R-ZE]
- Resolution: [e.g. 720p]
- FPS: [e.g. 5]
**Additional context**
Add any other context about the problem here.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,107 @@
name: Support Request
description: Support for Frigate setup or configuration
title: "[Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: ffprobe
attributes:
label: FFprobe output from your camera
description: Run `ffprobe <camera_url>` and provide output below
render: shell
validations:
required: true
- type: textarea
id: stats
attributes:
label: Frigate stats
description: Output from frigate's /api/stats endpoint
render: json
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network connection
options:
- Wired
- Wireless
- Mixed
validations:
required: true
- type: input
id: camera
attributes:
label: Camera make and model
description: Dahua, hikvision, amcrest, reolink, etc and model number
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

2
.gitignore vendored
View File

@@ -6,7 +6,9 @@ debug
config/config.yml config/config.yml
models models
*.mp4 *.mp4
*.ts
*.db *.db
*.csv
frigate/version.py frigate/version.py
web/build web/build
web/node_modules web/node_modules

View File

@@ -3,7 +3,7 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version: version:
echo "VERSION='0.9.0-$(COMMIT_HASH)'" > frigate/version.py echo "VERSION='0.10.0-$(COMMIT_HASH)'" > frigate/version.py
web: web:
docker build --tag frigate-web --file docker/Dockerfile.web web/ docker build --tag frigate-web --file docker/Dockerfile.web web/

View File

@@ -48,3 +48,17 @@ This may need to be in a custom location if network storage is used for the medi
If using a custom model, the width and height will need to be specified. If using a custom model, the width and height will need to be specified.
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model. The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
```yaml
model:
labelmap:
2: vehicle
3: vehicle
5: vehicle
7: vehicle
15: animal
16: animal
17: animal
```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.

View File

@@ -19,7 +19,7 @@ output_args:
rtmp: -c:v libx264 -an -f flv rtmp: -c:v libx264 -an -f flv
``` ```
### RTMP Cameras (Reolink 410/520 and possibly others) ### RTMP Cameras
The input parameters need to be adjusted for RTMP cameras The input parameters need to be adjusted for RTMP cameras
@@ -28,6 +28,46 @@ ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv
``` ```
### Reolink 410/520 (possibly others)
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink.
```yaml
cameras:
reolink:
ffmpeg:
hwaccel_args:
input_args:
- -avoid_negative_ts
- make_zero
- -fflags
- nobuffer+genpts+discardcorrupt
- -flags
- low_delay
- -strict
- experimental
- -analyzeduration
- 1000M
- -probesize
- 1000M
- -rw_timeout
- "5000000"
inputs:
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password
roles:
- record
- rtmp
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password
roles:
- detect
detect:
width: 640
height: 480
fps: 7
```
![Resolutions](/img/reolink-settings.png)
### Blue Iris RTSP Cameras ### Blue Iris RTSP Cameras
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras You will need to remove `nobuffer` flag for Blue Iris RTSP cameras

View File

@@ -31,6 +31,7 @@ detectors:
``` ```
### Native Coral (Dev Board) ### Native Coral (Dev Board)
_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_
```yaml ```yaml
detectors: detectors:

View File

@@ -48,8 +48,8 @@ mqtt:
# Optional: user # Optional: user
user: mqtt_user user: mqtt_user
# Optional: password # Optional: password
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}. # NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
# eg. password: '{FRIGATE_MQTT_PASSWORD}' # e.g. password: '{FRIGATE_MQTT_PASSWORD}'
password: password password: password
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None) # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
tls_ca_certs: /path/to/ca.crt tls_ca_certs: /path/to/ca.crt
@@ -159,6 +159,8 @@ detect:
enabled: True enabled: True
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate) # Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25 max_disappeared: 25
# Optional: Frequency for running detection on stationary objects (default: 10x the frame rate)
stationary_interval: 50
# Optional: Object configuration # Optional: Object configuration
# NOTE: Can be overridden at the camera level # NOTE: Can be overridden at the camera level
@@ -192,10 +194,14 @@ motion:
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. # 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. # The value should be between 1 and 255.
threshold: 25 threshold: 25
# Optional: Minimum size in pixels in the resized motion image that counts as motion (default: ~0.17% of the motion frame area) # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: 30)
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will
# moving objects. # make motion detection more sensitive to smaller moving objects.
contour_area: 100 # As a rule of thumb:
# - 15 - high sensitivity
# - 30 - medium sensitivity
# - 50 - low sensitivity
contour_area: 30
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below) # 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. # 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. # Too low and a fast moving person wont be detected as motion.
@@ -205,10 +211,10 @@ motion:
# Low values will cause things like moving shadows to be detected as motion for longer. # 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/ # https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
frame_alpha: 0.2 frame_alpha: 0.2
# Optional: Height of the resized motion frame (default: 1/6th of the original frame height, but no less than 180) # Optional: Height of the resized motion frame (default: 50)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage. # This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense
# Lower values result in less CPU, but small changes may not register as motion. # of higher CPU usage. Lower values result in less CPU, but small changes may not register as motion.
frame_height: 180 frame_height: 50
# Optional: motion mask # Optional: motion mask
# NOTE: see docs for more detailed info on creating masks # NOTE: see docs for more detailed info on creating masks
mask: 0,900,1080,900,1080,1920,0,1920 mask: 0,900,1080,900,1080,1920,0,1920
@@ -319,7 +325,7 @@ cameras:
# Required: A list of input streams for the camera. See documentation for more information. # Required: A list of input streams for the camera. See documentation for more information.
inputs: inputs:
# Required: the path to the stream # Required: the path to the stream
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {} # NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {}
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - 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,rtmp # Required: list of roles for this stream. valid values are: detect,record,rtmp
# NOTICE: In addition to assigning the record, and rtmp roles, # NOTICE: In addition to assigning the record, and rtmp roles,

View File

@@ -36,7 +36,7 @@ motion:
- 0,461,3,0,1919,0,1919,843,1699,492,1344 - 0,461,3,0,1919,0,1919,843,1699,492,1344
``` ```
![poly](/img/example-mask-poly.png) ![poly](/img/example-mask-poly-min.png)
### Further Clarification ### Further Clarification

View File

@@ -22,4 +22,4 @@ record:
This configuration will retain recording segments that overlap with events for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs. This configuration will retain recording segments that overlap with events for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
When `retain_days` is set to `0`, events will have up to `max_seconds` (defaults to 5 minutes) of recordings retained. Increasing `retain_days` to `1` will allow events to exceed the `max_seconds` limitation of up to 1 day. When `retain_days` is set to `0`, segments will be deleted from the cache if no events are in progress

View File

@@ -20,6 +20,10 @@ camera:
required_zones: required_zones:
- entire_yard - entire_yard
- front_yard_street - front_yard_street
snapshots:
required_zones:
- entire_yard
- front_yard_street
zones: zones:
entire_yard: entire_yard:
coordinates: ... (everywhere you want a person) coordinates: ... (everywhere you want a person)
@@ -31,4 +35,4 @@ camera:
- car - car
``` ```
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get clips for person objects that enter anywhere in the yard, and clips for cars only if they enter the street. Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.

View File

@@ -13,13 +13,13 @@ A solid green image means that frigate has not received any frames from ffmpeg.
### How can I get sound or audio in my recordings? ### How can I get sound or audio in my recordings?
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#ffmpeg). By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
### My mjpeg stream or snapshots look green and crazy ### 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. 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.
![mismatched-resolution](/img/mismatched-resolution.jpg) ![mismatched-resolution](/img/mismatched-resolution-min.jpg)
### I can't view events or recordings in the Web UI. ### I can't view events or recordings in the Web UI.

View File

@@ -17,7 +17,7 @@ The ideal resolution for detection is one where the objects you want to detect f
Larger resolutions **do** improve performance if the objects are very small in the frame. Larger resolutions **do** improve performance if the objects are very small in the frame.
![Resolutions](/img/resolutions.png) ![Resolutions](/img/resolutions-min.jpg)
### Example Camera Configuration ### Example Camera Configuration

View File

@@ -15,13 +15,13 @@ Frigate is designed to track objects as they move and over-masking can prevent i
::: :::
For example, you could create multiple zones that cover your driveway. For cars, you would only notify if entered_zones has more than 1 zone. For person, you would notify regardless of the number of entered_zones. To only be notified of cars that enter your driveway from the street, you could create multiple zones that cover your driveway. For cars, you would only notify if `entered_zones` from the events MQTT topic has more than 1 zone.
See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation. See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation to see how to restrict zones to certain object types.
You can also create a zone for the entrance of your driveway and only save an event if that zone is in the list of entered_zones when the object is a car. ![Driveway Zones](/img/driveway_zones-min.png)
![Driveway Zones](/img/driveway_zones.png) To limit snapshots and events, you can list the zone for the entrance of your driveway under `required_zones` in your configuration file. Example below.
```yaml ```yaml
camera: camera:

View File

@@ -9,28 +9,31 @@ Cameras that output H.264 video and AAC audio will offer the most compatibility
I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options. I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options.
Many users have reported various issues with Reolink cameras, so I do not recommend them. If you are using Reolink, I suggest the [Reolink specific configuration](configuration/camera_specific#reolink-410520-possibly-others). Wifi cameras are also not recommended. Their streams are less reliable and cause connection loss and/or lost video data.
Here are some of the camera's I recommend: Here are some of the camera's I recommend:
- [Loryta(Dahua) T5442TM-AS-LED](https://amzn.to/2Wck2hQ) (affiliate link) - <a href="https://amzn.to/3uFLtxB" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) T5442TM-AS-LED</a> (affiliate link)
- [Loryta(Dahua) IPC-T5442TM-AS](https://amzn.to/39FODrm) (affiliate link) - <a href="https://amzn.to/3isJ3gU" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) IPC-T5442TM-AS</a> (affiliate link)
- [Amcrest IP5M-T1179EW-28MM](https://amzn.to/39H1zgt) (affiliate link) - <a href="https://amzn.to/2ZWNWIA" target="_blank" rel="nofollow noopener sponsored">Amcrest IP5M-T1179EW-28MM</a> (affiliate link)
I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
## Server ## Server
My current favorite is the Minisforum GK50 because the dual NICs allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. My current favorite is the Odyssey X86 Blue J4125 because the Coral M.2 compatibility and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
| Name | Inference Speed | Notes | | Name | Inference Speed | Coral Compatibility | Notes |
| ------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | -------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [Minisforum GK41](https://amzn.to/3kI0njr) (affiliate link) | 9-10ms | Great alternative to a NUC. Easily handles several 1080p cameras. | | <a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Odyssey X86 Blue J4125</a> (affiliate link) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| [Minisforum GK50](https://amzn.to/3m49yKk) (affiliate link) | 9-10ms | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. | | <a href="https://amzn.to/3oxEC8m" target="_blank" rel="nofollow noopener sponsored">Minisforum GK41</a> (affiliate link) | 9-10ms | USB | Great alternative to a NUC. Easily handles several 1080p cameras. |
| [Intel NUC](https://amzn.to/3kImYMT) (affiliate link) | 8-10ms | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. | | <a href="https://amzn.to/3ixJFlb" target="_blank" rel="nofollow noopener sponsored">Minisforum GK50</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| [BMAX B2 Plus](https://amzn.to/3uccBnD) (affiliate link) | 10-12ms | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. | | <a href="https://amzn.to/3l7vCEI" target="_blank" rel="nofollow noopener sponsored">Intel NUC</a> (affiliate link) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. |
| [Atomic Pi](https://amzn.to/3i9YRVw) (affiliate link) | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. | | <a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">BMAX B2 Plus</a> (affiliate link) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
| [Raspberry Pi 3B (32bit)](https://amzn.to/3lZUi16) (affiliate link) | 60ms | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. | | <a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Atomic Pi</a> (affiliate link) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| [Raspberry Pi 4 (32bit)](https://amzn.to/2ZpgDNW) (affiliate link) | 15-20ms | Can handle a small number of cameras. The 2GB version runs fine. | | <a href="https://amzn.to/2WIpwRU" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 3B (32bit)</a> (affiliate link) | 60ms | USB | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. |
| [Raspberry Pi 4 (64bit)](https://amzn.to/2ZpgDNW) (affiliate link) | 10-15ms | Can handle a small number of cameras. The 2GB version runs fine. | | <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (32bit)</a> (affiliate link) | 15-20ms | USB | Can handle a small number of cameras. The 2GB version runs fine. |
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (64bit)</a> (affiliate link) | 10-15ms | USB | Can handle a small number of cameras. The 2GB version runs fine. |
## Google Coral TPU ## Google Coral TPU
@@ -41,3 +44,25 @@ The USB version is compatible with the widest variety of hardware and does not r
The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai
A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed. A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed.
### What does Frigate use the CPU for and what does it use the Coral for? (ELI5 Version)
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
CPU Usage: I am a CPU, Mendel is a Google Coral
My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it.
Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score!
_What happens when I increase the resolution of my camera?_
However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby.
Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it.
### Do hwaccel args help if I am using a Coral?
YES! The Coral does not help with decoding video streams.
Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://blog.video.ibm.com/streaming-video-tips/keyframes-interframe-video-compression/). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work.

View File

@@ -20,6 +20,6 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
## Screenshots ## Screenshots
![Media Browser](/img/media_browser.png) ![Media Browser](/img/media_browser-min.png)
![Notification](/img/notification.png) ![Notification](/img/notification-min.png)

View File

@@ -17,13 +17,68 @@ Frigate runs best with docker installed on bare metal debian-based distributions
Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help. Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help.
### Storage
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder.
- `/dev/shm`: It is not recommended to modify this directory or map it with docker. This is the location for raw decoded frames in shared memory and it's size is impacted by the `shm-size` calculations below.
- `/config/config.yml`: Default location of the config file.
#### Common docker compose storage configurations
Writing to a local disk or external USB drive:
```yaml
version: "3.9"
services:
frigate:
...
volumes:
- /path/to/your/config.yml:/config/config.yml:ro
- /path/to/your/storage:/media/frigate
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
target: /tmp/cache
tmpfs:
size: 1000000000
...
```
Writing to a network drive with database on a local drive:
```yaml
version: "3.9"
services:
frigate:
...
volumes:
- /path/to/your/config.yml:/config/config.yml:ro
- /path/to/network/storage:/media/frigate
- /path/to/local/disk:/db
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
target: /tmp/cache
tmpfs:
size: 1000000000
...
```
frigate.yml
```yaml
database:
path: /db/frigate.db
```
### Calculating required shm-size ### Calculating required shm-size
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m. Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
The default shm-size of 64m is fine for setups with 2 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size. The default shm-size of 64m is fine for setups with 2 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it is likely 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: You can calculate the necessary shm-size for each camera with the following formula using the resolution specified for detect:
``` ```
(width * height * 1.5 * 9 + 270480)/1048576 = <shm size in mb> (width * height * 1.5 * 9 + 270480)/1048576 = <shm size in mb>
@@ -35,7 +90,7 @@ The shm size cannot be set per container for Home Assistant Addons. You must set
By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options). By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options).
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with [this](https://amzn.to/2XTEqp7) (affiliate link). Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with <a href="https://amzn.to/3a2mH0P" target="_blank" rel="nofollow noopener sponsored">this</a> (affiliate link).
## Docker ## Docker
@@ -102,7 +157,7 @@ docker run -d \
:::caution :::caution
Due to limitations in Home Assistant Operating System, Frigate cannot utilize external storage for recordings or snapshots. Due to limitations in Home Assistant Operating System, utilizing external storage for recordings or snapshots requires [modifying udev rules manually](https://community.home-assistant.io/t/solved-mount-usb-drive-in-hassio-to-be-used-on-the-media-folder-with-udev-customization/258406/46).
::: :::

View File

@@ -29,6 +29,16 @@ module.exports = {
label: 'Docs', label: 'Docs',
position: 'left', position: 'left',
}, },
{
href: 'https://frigate.video',
label: 'Website',
position: 'right',
},
{
href: 'https://demo.frigate.video',
label: 'Demo',
position: 'right',
},
{ {
href: 'https://github.com/blakeblackshear/frigate', href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub', label: 'GitHub',

54
docs/package-lock.json generated
View File

@@ -2469,31 +2469,11 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM="
}, },
"ansi-align": { "ansi-align": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"requires": { "requires": {
"string-width": "^3.0.0" "string-width": "^4.1.0"
},
"dependencies": {
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
}
} }
}, },
"ansi-colors": { "ansi-colors": {
@@ -2902,15 +2882,15 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
}, },
"boxen": { "boxen": {
"version": "5.0.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==", "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"requires": { "requires": {
"ansi-align": "^3.0.0", "ansi-align": "^3.0.0",
"camelcase": "^6.2.0", "camelcase": "^6.2.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"cli-boxes": "^2.2.1", "cli-boxes": "^2.2.1",
"string-width": "^4.2.0", "string-width": "^4.2.2",
"type-fest": "^0.20.2", "type-fest": "^0.20.2",
"widest-line": "^3.1.0", "widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
@@ -6826,9 +6806,9 @@
"integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E=" "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
}, },
"nth-check": { "nth-check": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz",
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==",
"requires": { "requires": {
"boolbase": "^1.0.0" "boolbase": "^1.0.0"
} }
@@ -7650,9 +7630,9 @@
"integrity": "sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg==" "integrity": "sha512-w23ch4f75V1Tnz8DajsYKvY5lF7H1+WvzvLUcF0paFxkTHSp42RS0H5CttdN2Q8RR3DRGZ9v5xD/h3n8C8kGmg=="
}, },
"prismjs": { "prismjs": {
"version": "1.24.1", "version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==" "integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
}, },
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
@@ -9397,9 +9377,9 @@
}, },
"dependencies": { "dependencies": {
"ansi-regex": { "ansi-regex": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
} }
} }
}, },

View File

@@ -12,8 +12,8 @@
"clear": "docusaurus clear" "clear": "docusaurus clear"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^2.0.0-beta.ff31de0ff", "@docusaurus/core": "^2.0.0-beta.6",
"@docusaurus/preset-classic": "^2.0.0-beta.ff31de0ff", "@docusaurus/preset-classic": "^2.0.0-beta.6",
"@mdx-js/react": "^1.6.21", "@mdx-js/react": "^1.6.21",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

BIN
docs/static/img/driveway_zones-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/static/img/media_browser-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/static/img/notification-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
docs/static/img/reolink-settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/static/img/resolutions-min.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -12,6 +12,7 @@ import yaml
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError
from frigate.config import DetectorTypeEnum, FrigateConfig from frigate.config import DetectorTypeEnum, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
@@ -70,6 +71,9 @@ class FrigateApp:
self.config = user_config.runtime_config self.config = user_config.runtime_config
for camera_name in self.config.cameras.keys(): for camera_name in self.config.cameras.keys():
# generage the ffmpeg commands
self.config.cameras[camera_name].create_ffmpeg_cmds()
# create camera_metrics # create camera_metrics
self.camera_metrics[camera_name] = { self.camera_metrics[camera_name] = {
"camera_fps": mp.Value("d", 0.0), "camera_fps": mp.Value("d", 0.0),
@@ -85,29 +89,6 @@ class FrigateApp:
"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.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): def set_log_levels(self):
logging.getLogger().setLevel(self.config.logger.default.value.upper()) logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items(): for log, level in self.config.logger.logs.items():
@@ -320,12 +301,23 @@ class FrigateApp:
try: try:
self.init_config() self.init_config()
except Exception as e: except Exception as e:
print(f"Error parsing config: {e}") print("*************************************************************")
print("*************************************************************")
print("*** Your config file is not valid! ***")
print("*** Please check the docs at ***")
print("*** https://docs.frigate.video/configuration/index ***")
print("*************************************************************")
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
print(e)
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
self.log_process.terminate() self.log_process.terminate()
sys.exit(1) sys.exit(1)
self.set_environment_vars() self.set_environment_vars()
self.ensure_dirs() self.ensure_dirs()
self.check_config()
self.set_log_levels() self.set_log_levels()
self.init_queues() self.init_queues()
self.init_database() self.init_database()

BIN
frigate/birdseye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -12,7 +12,7 @@ import yaml
from pydantic import BaseModel, Extra, Field, validator from pydantic import BaseModel, Extra, Field, validator
from pydantic.fields import PrivateAttr from pydantic.fields import PrivateAttr
from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR from frigate.const import BASE_DIR, CACHE_DIR
from frigate.edgetpu import load_labels from frigate.edgetpu import load_labels
from frigate.util import create_mask, deep_merge from frigate.util import create_mask, deep_merge
@@ -103,10 +103,10 @@ class MotionConfig(FrigateBaseModel):
ge=1, ge=1,
le=255, le=255,
) )
contour_area: Optional[int] = Field(title="Contour Area") contour_area: Optional[int] = Field(default=30, title="Contour Area")
delta_alpha: float = Field(default=0.2, title="Delta Alpha") delta_alpha: float = Field(default=0.2, title="Delta Alpha")
frame_alpha: float = Field(default=0.2, title="Frame Alpha") frame_alpha: float = Field(default=0.2, title="Frame Alpha")
frame_height: Optional[int] = Field(title="Frame Height") frame_height: Optional[int] = Field(default=50, title="Frame Height")
mask: Union[str, List[str]] = Field( mask: Union[str, List[str]] = Field(
default="", title="Coordinates polygon for the motion mask." default="", title="Coordinates polygon for the motion mask."
) )
@@ -119,15 +119,6 @@ class RuntimeMotionConfig(MotionConfig):
def __init__(self, **config): def __init__(self, **config):
frame_shape = config.get("frame_shape", (1, 1)) frame_shape = config.get("frame_shape", (1, 1))
if "frame_height" not in config:
config["frame_height"] = max(frame_shape[0] // 6, 180)
if "contour_area" not in config:
frame_width = frame_shape[1] * config["frame_height"] / frame_shape[0]
config["contour_area"] = (
config["frame_height"] * frame_width * 0.00173611111
)
mask = config.get("mask", "") mask = config.get("mask", "")
config["raw_mask"] = mask config["raw_mask"] = mask
@@ -162,6 +153,9 @@ class DetectConfig(FrigateBaseModel):
max_disappeared: Optional[int] = Field( max_disappeared: Optional[int] = Field(
title="Maximum number of frames the object can dissapear before detection ends." title="Maximum number of frames the object can dissapear before detection ends."
) )
stationary_interval: Optional[int] = Field(
title="Frame interval for checking stationary objects."
)
class FilterConfig(FrigateBaseModel): class FilterConfig(FrigateBaseModel):
@@ -495,6 +489,7 @@ class CameraConfig(FrigateBaseModel):
timestamp_style: TimestampStyleConfig = Field( timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration." default_factory=TimestampStyleConfig, title="Timestamp style configuration."
) )
_ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()
def __init__(self, **config): def __init__(self, **config):
# Set zone colors # Set zone colors
@@ -505,6 +500,10 @@ class CameraConfig(FrigateBaseModel):
for idx, (name, z) in enumerate(config["zones"].items()) for idx, (name, z) in enumerate(config["zones"].items())
} }
# add roles to the input if there is only one
if len(config["ffmpeg"]["inputs"]) == 1:
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
super().__init__(**config) super().__init__(**config)
@property @property
@@ -517,6 +516,9 @@ class CameraConfig(FrigateBaseModel):
@property @property
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]: def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
return self._ffmpeg_cmds
def create_ffmpeg_cmds(self):
ffmpeg_cmds = [] ffmpeg_cmds = []
for ffmpeg_input in self.ffmpeg.inputs: for ffmpeg_input in self.ffmpeg.inputs:
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input) ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
@@ -524,7 +526,7 @@ class CameraConfig(FrigateBaseModel):
continue continue
ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd}) ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
return ffmpeg_cmds self._ffmpeg_cmds = ffmpeg_cmds
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
ffmpeg_output_args = [] ffmpeg_output_args = []
@@ -560,6 +562,7 @@ class CameraConfig(FrigateBaseModel):
if isinstance(self.ffmpeg.output_args.record, list) if isinstance(self.ffmpeg.output_args.record, list)
else self.ffmpeg.output_args.record.split(" ") else self.ffmpeg.output_args.record.split(" ")
) )
ffmpeg_output_args = ( ffmpeg_output_args = (
record_args record_args
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"] + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
@@ -740,6 +743,11 @@ class FrigateConfig(FrigateBaseModel):
if camera_config.detect.max_disappeared is None: if camera_config.detect.max_disappeared is None:
camera_config.detect.max_disappeared = max_disappeared camera_config.detect.max_disappeared = max_disappeared
# Default stationary_interval configuration
stationary_interval = camera_config.detect.fps * 10
if camera_config.detect.stationary_interval is None:
camera_config.detect.stationary_interval = stationary_interval
# FFMPEG input substitution # FFMPEG input substitution
for input in camera_config.ffmpeg.inputs: for input in camera_config.ffmpeg.inputs:
input.path = input.path.format(**FRIGATE_ENV_VARS) input.path = input.path.format(**FRIGATE_ENV_VARS)
@@ -787,6 +795,20 @@ class FrigateConfig(FrigateBaseModel):
**camera_config.motion.dict(exclude_unset=True), **camera_config.motion.dict(exclude_unset=True),
) )
# check runtime config
assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
)
if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError(
f"Camera {name} has record enabled, but record is not assigned to an input."
)
if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
raise ValueError(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
config.cameras[name] = camera_config config.cameras[name] = camera_config
return config return config

View File

@@ -30,6 +30,11 @@ class EventProcessor(threading.Thread):
self.stop_event = stop_event self.stop_event = stop_event
def run(self): def run(self):
# set an end_time on events without an end_time on startup
Event.update(end_time=Event.start_time + 30).where(
Event.end_time == None
).execute()
while not self.stop_event.is_set(): while not self.stop_event.is_set():
try: try:
event_type, camera, event_data = self.event_queue.get(timeout=10) event_type, camera, event_data = self.event_queue.get(timeout=10)
@@ -38,14 +43,35 @@ class EventProcessor(threading.Thread):
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}") logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
event_config: EventsConfig = self.config.cameras[camera].record.events
if event_type == "start": if event_type == "start":
self.events_in_process[event_data["id"]] = event_data self.events_in_process[event_data["id"]] = event_data
if event_type == "end": elif event_type == "update":
event_config: EventsConfig = self.config.cameras[camera].record.events self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly
if event_data["has_clip"] or event_data["has_snapshot"]: if event_data["has_clip"] or event_data["has_snapshot"]:
Event.create( Event.replace(
id=event_data["id"],
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
end_time=None,
top_score=event_data["top_score"],
false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"],
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).execute()
elif event_type == "end":
if event_data["has_clip"] or event_data["has_snapshot"]:
Event.replace(
id=event_data["id"], id=event_data["id"],
label=event_data["label"], label=event_data["label"],
camera=camera, camera=camera,
@@ -60,11 +86,15 @@ class EventProcessor(threading.Thread):
area=event_data["area"], area=event_data["area"],
has_clip=event_data["has_clip"], has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"], has_snapshot=event_data["has_snapshot"],
) ).execute()
del self.events_in_process[event_data["id"]] del self.events_in_process[event_data["id"]]
self.event_processed_queue.put((event_data["id"], camera)) self.event_processed_queue.put((event_data["id"], camera))
# set an end_time on events without an end_time before exiting
Event.update(end_time=datetime.datetime.now().timestamp()).where(
Event.end_time == None
).execute()
logger.info(f"Exiting event processor...") logger.info(f"Exiting event processor...")

View File

@@ -1,6 +1,7 @@
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import copy
import json import json
import glob import glob
import logging import logging
@@ -190,7 +191,7 @@ def event_snapshot(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
jpg_bytes = None jpg_bytes = None
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == id, Event.end_time != None)
if not event.has_snapshot: if not event.has_snapshot:
return "Snapshot not available", 404 return "Snapshot not available", 404
# read snapshot from disk # read snapshot from disk
@@ -321,7 +322,7 @@ def config():
# add in the ffmpeg_cmds # add in the ffmpeg_cmds
for camera_name, camera in current_app.frigate_config.cameras.items(): for camera_name, camera in current_app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name] camera_dict = config["cameras"][camera_name]
camera_dict["ffmpeg_cmds"] = camera.ffmpeg_cmds camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
for cmd in camera_dict["ffmpeg_cmds"]: for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"]) cmd["cmd"] = " ".join(cmd["cmd"])
@@ -697,7 +698,10 @@ def vod_event(id):
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4") clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
if not os.path.isfile(clip_path): if not os.path.isfile(clip_path):
return vod_ts(event.camera, event.start_time, event.end_time) end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time
)
return vod_ts(event.camera, event.start_time, end_ts)
duration = int((event.end_time - event.start_time) * 1000) duration = int((event.end_time - event.start_time) * 1000)
return jsonify( return jsonify(

View File

@@ -23,6 +23,7 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR, interpolation=cv2.INTER_LINEAR,
) )
self.mask = np.where(resized_mask == [0]) self.mask = np.where(resized_mask == [0])
self.save_images = False
def detect(self, frame): def detect(self, frame):
motion_boxes = [] motion_boxes = []
@@ -36,10 +37,15 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR, interpolation=cv2.INTER_LINEAR,
) )
# TODO: can I improve the contrast of the grayscale image here? # Improve contrast
minval = np.percentile(resized_frame, 4)
# convert to grayscale maxval = np.percentile(resized_frame, 96)
# resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY) # don't adjust if the image is a single color
if minval < maxval:
resized_frame = np.clip(resized_frame, minval, maxval)
resized_frame = (
((resized_frame - minval) / (maxval - minval)) * 255
).astype(np.uint8)
# mask frame # mask frame
resized_frame[self.mask] = [255] resized_frame[self.mask] = [255]
@@ -49,6 +55,8 @@ class MotionDetector:
if self.frame_counter < 30: if self.frame_counter < 30:
self.frame_counter += 1 self.frame_counter += 1
else: else:
if self.save_images:
self.frame_counter += 1
# compare to average # compare to average
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
@@ -58,7 +66,6 @@ class MotionDetector:
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha) cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
# compute the threshold image for the current frame # compute the threshold image for the current frame
# TODO: threshold
current_thresh = cv2.threshold( current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY
)[1] )[1]
@@ -75,8 +82,10 @@ class MotionDetector:
# dilate the thresholded image to fill in holes, then find contours # dilate the thresholded image to fill in holes, then find contours
# on thresholded image # on thresholded image
thresh = cv2.dilate(thresh, None, iterations=2) thresh_dilated = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = cv2.findContours(
thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
cnts = imutils.grab_contours(cnts) cnts = imutils.grab_contours(cnts)
# loop over the contours # loop over the contours
@@ -94,6 +103,35 @@ class MotionDetector:
) )
) )
if self.save_images:
thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR)
# print("--------")
# print(self.frame_counter)
for c in cnts:
contour_area = cv2.contourArea(c)
# print(contour_area)
if contour_area > self.config.contour_area:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(
thresh_dilated,
(x, y),
(x + w, y + h),
(0, 0, 255),
2,
)
# print("--------")
image_row_1 = cv2.hconcat(
[
cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR),
cv2.cvtColor(avg_delta_image, cv2.COLOR_GRAY2BGR),
]
)
image_row_2 = cv2.hconcat(
[cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), thresh_dilated]
)
combined_image = cv2.vconcat([image_row_1, image_row_2])
cv2.imwrite(f"motion/motion-{self.frame_counter}.jpg", combined_image)
if len(motion_boxes) > 0: if len(motion_boxes) > 0:
self.motion_frame_count += 1 self.motion_frame_count += 1
if self.motion_frame_count >= 10: if self.motion_frame_count >= 10:

View File

@@ -603,6 +603,8 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue.put(("start", camera, obj.to_dict())) self.event_queue.put(("start", camera, obj.to_dict()))
def update(camera, obj: TrackedObject, current_frame_time): def update(camera, obj: TrackedObject, current_frame_time):
obj.has_snapshot = self.should_save_snapshot(camera, obj)
obj.has_clip = self.should_retain_recording(camera, obj)
after = obj.to_dict() after = obj.to_dict()
message = { message = {
"before": obj.previous, "before": obj.previous,
@@ -613,6 +615,9 @@ class TrackedObjectProcessor(threading.Thread):
f"{self.topic_prefix}/events", json.dumps(message), retain=False f"{self.topic_prefix}/events", json.dumps(message), retain=False
) )
obj.previous = after obj.previous = after
self.event_queue.put(
("update", camera, obj.to_dict(include_thumbnail=True))
)
def end(camera, obj: TrackedObject, current_frame_time): def end(camera, obj: TrackedObject, current_frame_time):
# populate has_snapshot # populate has_snapshot

View File

@@ -13,7 +13,7 @@ import numpy as np
from scipy.spatial import distance as dist from scipy.spatial import distance as dist
from frigate.config import DetectConfig from frigate.config import DetectConfig
from frigate.util import draw_box_with_label from frigate.util import intersection_over_union
class ObjectTracker: class ObjectTracker:
@@ -27,6 +27,7 @@ class ObjectTracker:
id = f"{obj['frame_time']}-{rand_id}" id = f"{obj['frame_time']}-{rand_id}"
obj["id"] = id obj["id"] = id
obj["start_time"] = obj["frame_time"] obj["start_time"] = obj["frame_time"]
obj["motionless_count"] = 0
self.tracked_objects[id] = obj self.tracked_objects[id] = obj
self.disappeared[id] = 0 self.disappeared[id] = 0
@@ -36,6 +37,13 @@ class ObjectTracker:
def update(self, id, new_obj): def update(self, id, new_obj):
self.disappeared[id] = 0 self.disappeared[id] = 0
if (
intersection_over_union(self.tracked_objects[id]["box"], new_obj["box"])
> 0.9
):
self.tracked_objects[id]["motionless_count"] += 1
else:
self.tracked_objects[id]["motionless_count"] = 0
self.tracked_objects[id].update(new_obj) self.tracked_objects[id].update(new_obj)
def match_and_update(self, frame_time, new_objects): def match_and_update(self, frame_time, new_objects):

View File

@@ -104,7 +104,7 @@ class BirdsEyeFrameManager:
self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16 self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16
# find and copy the logo on the blank frame # find and copy the logo on the blank frame
logo_files = glob.glob("/opt/frigate/web/apple-touch-icon.*.png") logo_files = glob.glob("/opt/frigate/frigate/birdseye.png")
frigate_logo = None frigate_logo = None
if len(logo_files) > 0: if len(logo_files) > 0:
frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED)

View File

@@ -1,4 +1,5 @@
import datetime import datetime
import time
import itertools import itertools
import logging import logging
import os import os
@@ -7,6 +8,7 @@ import shutil
import string import string
import subprocess as sp import subprocess as sp
import threading import threading
from collections import defaultdict
from pathlib import Path from pathlib import Path
import psutil import psutil
@@ -43,9 +45,11 @@ class RecordingMaintainer(threading.Thread):
self.name = "recording_maint" self.name = "recording_maint"
self.config = config self.config = config
self.stop_event = stop_event self.stop_event = stop_event
self.first_pass = True
self.end_time_cache = {}
def move_files(self): def move_files(self):
recordings = [ cache_files = [
d d
for d in os.listdir(CACHE_DIR) for d in os.listdir(CACHE_DIR)
if os.path.isfile(os.path.join(CACHE_DIR, d)) if os.path.isfile(os.path.join(CACHE_DIR, d))
@@ -66,7 +70,9 @@ class RecordingMaintainer(threading.Thread):
except: except:
continue continue
for f in recordings: # group recordings by camera
grouped_recordings = defaultdict(list)
for f in cache_files:
# Skip files currently in use # Skip files currently in use
if f in files_in_use: if f in files_in_use:
continue continue
@@ -76,45 +82,130 @@ class RecordingMaintainer(threading.Thread):
camera, date = basename.rsplit("-", maxsplit=1) camera, date = basename.rsplit("-", maxsplit=1)
start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S") start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
# Just delete files if recordings are turned off grouped_recordings[camera].append(
if ( {
not camera in self.config.cameras "cache_path": cache_path,
or not self.config.cameras[camera].record.enabled "start_time": start_time,
): }
Path(cache_path).unlink(missing_ok=True)
continue
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
else:
logger.warning(f"Discarding a corrupt recording segment: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
directory = os.path.join(
RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera
) )
if not os.path.exists(directory): # delete all cached files past the most recent 5
os.makedirs(directory) keep_count = 5
for camera in grouped_recordings.keys():
if len(grouped_recordings[camera]) > keep_count:
sorted_recordings = sorted(
grouped_recordings[camera], key=lambda i: i["start_time"]
)
to_remove = sorted_recordings[:-keep_count]
for f in to_remove:
Path(f["cache_path"]).unlink(missing_ok=True)
self.end_time_cache.pop(f["cache_path"], None)
grouped_recordings[camera] = sorted_recordings[-keep_count:]
file_name = f"{start_time.strftime('%M.%S.mp4')}" for camera, recordings in grouped_recordings.items():
file_path = os.path.join(directory, file_name) # get all events with the end time after the start of the oldest cache file
# or with end_time None
events: Event = (
Event.select()
.where(
Event.camera == camera,
(Event.end_time == None)
| (Event.end_time >= recordings[0]["start_time"]),
Event.has_clip,
)
.order_by(Event.start_time)
)
for r in recordings:
cache_path = r["cache_path"]
start_time = r["start_time"]
# Just delete files if recordings are turned off
if (
not camera in self.config.cameras
or not self.config.cameras[camera].record.enabled
):
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
continue
if cache_path in self.end_time_cache:
end_time, duration = self.end_time_cache[cache_path]
else:
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
self.end_time_cache[cache_path] = (end_time, duration)
else:
logger.warning(f"Discarding a corrupt recording segment: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
# if cached file's start_time is earlier than the retain_days for the camera
if start_time <= (
(
datetime.datetime.now()
- datetime.timedelta(
days=self.config.cameras[camera].record.retain_days
)
)
):
# if the cached segment overlaps with the events:
overlaps = False
for event in events:
# if the event starts in the future, stop checking events
# and remove this segment
if event.start_time > end_time.timestamp():
overlaps = False
break
# if the event is in progress or ends after the recording starts, keep it
# and stop looking at events
if event.end_time is None or event.end_time >= start_time:
overlaps = True
break
if overlaps:
# move from cache to recordings immediately
self.store_segment(
camera,
start_time,
end_time,
duration,
cache_path,
)
# else retain_days includes this segment
else:
self.store_segment(
camera, start_time, end_time, duration, cache_path
)
def store_segment(self, camera, start_time, end_time, duration, cache_path):
directory = os.path.join(RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera)
if not os.path.exists(directory):
os.makedirs(directory)
file_name = f"{start_time.strftime('%M.%S.mp4')}"
file_path = os.path.join(directory, file_name)
try:
start_frame = datetime.datetime.now().timestamp()
# copy then delete is required when recordings are stored on some network drives # copy then delete is required when recordings are stored on some network drives
shutil.copyfile(cache_path, file_path) shutil.copyfile(cache_path, file_path)
logger.debug(
f"Copied {file_path} in {datetime.datetime.now().timestamp()-start_frame} seconds."
)
os.remove(cache_path) os.remove(cache_path)
rand_id = "".join( rand_id = "".join(
@@ -128,11 +219,34 @@ class RecordingMaintainer(threading.Thread):
end_time=end_time.timestamp(), end_time=end_time.timestamp(),
duration=duration, duration=duration,
) )
except Exception as e:
logger.error(f"Unable to store recording segment {cache_path}")
Path(cache_path).unlink(missing_ok=True)
logger.error(e)
# clear end_time cache
self.end_time_cache.pop(cache_path, None)
def run(self): def run(self):
# Check for new files every 5 seconds # Check for new files every 5 seconds
while not self.stop_event.wait(5): wait_time = 5
self.move_files() while not self.stop_event.wait(wait_time):
run_start = datetime.datetime.now().timestamp()
try:
self.move_files()
except Exception as e:
logger.error(
"Error occurred when attempting to maintain recording cache"
)
logger.error(e)
duration = datetime.datetime.now().timestamp() - run_start
wait_time = max(0, 5 - duration)
if wait_time == 0 and not self.first_pass:
logger.warning(
"Cache is taking longer than 5 seconds to clear. Your recordings disk may be too slow."
)
if self.first_pass:
self.first_pass = False
logger.info(f"Exiting recording maintenance...") logger.info(f"Exiting recording maintenance...")
@@ -203,7 +317,11 @@ class RecordingCleanup(threading.Thread):
events: Event = ( events: Event = (
Event.select() Event.select()
.where( .where(
Event.camera == camera, Event.end_time < expire_date, Event.has_clip Event.camera == camera,
# need to ensure segments for all events starting
# before the expire date are included
Event.start_time < expire_date,
Event.has_clip,
) )
.order_by(Event.start_time) .order_by(Event.start_time)
.objects() .objects()
@@ -224,9 +342,9 @@ class RecordingCleanup(threading.Thread):
keep = False keep = False
break break
# if the event ends after the recording starts, keep it # if the event is in progress or ends after the recording starts, keep it
# and stop looking at events # and stop looking at events
if event.end_time >= recording.start_time: if event.end_time is None or event.end_time >= recording.start_time:
keep = True keep = True
break break
@@ -267,17 +385,19 @@ class RecordingCleanup(threading.Thread):
# find all the recordings older than the oldest recording in the db # find all the recordings older than the oldest recording in the db
try: try:
oldest_recording = ( oldest_recording = Recordings.select().order_by(Recordings.start_time).get()
Recordings.select().order_by(Recordings.start_time.desc()).get()
)
oldest_timestamp = oldest_recording.start_time p = Path(oldest_recording.path)
oldest_timestamp = p.stat().st_mtime - 1
except DoesNotExist: except DoesNotExist:
oldest_timestamp = datetime.datetime.now().timestamp() oldest_timestamp = datetime.datetime.now().timestamp()
except FileNotFoundError:
logger.warning(f"Unable to find file from recordings database: {p}")
oldest_timestamp = datetime.datetime.now().timestamp()
logger.debug(f"Oldest recording in the db: {oldest_timestamp}") logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
process = sp.run( process = sp.run(
["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"], ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
capture_output=True, capture_output=True,
text=True, text=True,
) )

View File

@@ -702,7 +702,11 @@ class TestConfig(unittest.TestCase):
"inputs": [ "inputs": [
{ {
"path": "rtsp://10.0.0.1:554/video", "path": "rtsp://10.0.0.1:554/video",
"roles": ["detect", "clips"], "roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["clips"],
}, },
] ]
}, },
@@ -717,6 +721,87 @@ class TestConfig(unittest.TestCase):
self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_fails_on_missing_role(self):
config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
"rtmp": {"enabled": True},
}
},
}
frigate_config = FrigateConfig(**config)
self.assertRaises(ValueError, lambda: frigate_config.runtime_config)
def test_works_on_missing_role_multiple_cams(self):
config = {
"mqtt": {"host": "mqtt"},
"rtmp": {"enabled": False},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
},
"cam2": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
},
},
}
frigate_config = FrigateConfig(**config)
runtime_config = frigate_config.runtime_config
def test_global_detect(self): def test_global_detect(self):
config = { config = {
@@ -958,6 +1043,34 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].rtmp.enabled assert runtime_config.cameras["back"].rtmp.enabled
def test_global_rtmp_default(self):
config = {
"mqtt": {"host": "mqtt"},
"rtmp": {"enabled": False},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert not runtime_config.cameras["back"].rtmp.enabled
def test_global_live(self): def test_global_live(self):
config = { config = {

View File

@@ -0,0 +1,27 @@
import cv2
import numpy as np
from unittest import TestCase, main
from frigate.video import box_overlaps, reduce_boxes
class TestBoxOverlaps(TestCase):
def test_overlap(self):
assert box_overlaps((100, 100, 200, 200), (50, 50, 150, 150))
def test_overlap_2(self):
assert box_overlaps((50, 50, 150, 150), (100, 100, 200, 200))
def test_no_overlap(self):
assert not box_overlaps((100, 100, 200, 200), (250, 250, 350, 350))
class TestReduceBoxes(TestCase):
def test_cluster(self):
clusters = reduce_boxes(
[(144, 290, 221, 459), (225, 178, 426, 341), (343, 105, 584, 250)]
)
assert len(clusters) == 2
if __name__ == "__main__":
main(verbosity=2)

View File

@@ -191,7 +191,7 @@ def draw_box_with_label(
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2): def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
# size is the longest edge and divisible by 4 # size is the longest edge and divisible by 4
size = int(max(xmax - xmin, ymax - ymin) // 4 * 4 * multiplier) size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4)
# dont go any smaller than 300 # dont go any smaller than 300
if size < 300: if size < 300:
size = 300 size = 300

View File

@@ -3,18 +3,18 @@ import itertools
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import queue import queue
import subprocess as sp
import signal import signal
import subprocess as sp
import threading import threading
import time import time
from collections import defaultdict from collections import defaultdict
from setproctitle import setproctitle
from typing import Dict, List from typing import Dict, List
from cv2 import cv2
import numpy as np import numpy as np
from cv2 import cv2, reduce
from setproctitle import setproctitle
from frigate.config import CameraConfig from frigate.config import CameraConfig, DetectConfig
from frigate.edgetpu import RemoteObjectDetector from frigate.edgetpu import RemoteObjectDetector
from frigate.log import LogPipe from frigate.log import LogPipe
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
@@ -23,8 +23,11 @@ from frigate.util import (
EventsPerSecond, EventsPerSecond,
FrameManager, FrameManager,
SharedMemoryFrameManager, SharedMemoryFrameManager,
area,
calculate_region, calculate_region,
clipped, clipped,
intersection,
intersection_over_union,
listen, listen,
yuv_region_2_rgb, yuv_region_2_rgb,
) )
@@ -364,6 +367,7 @@ def track_camera(
frame_queue, frame_queue,
frame_shape, frame_shape,
model_shape, model_shape,
config.detect,
frame_manager, frame_manager,
motion_detector, motion_detector,
object_detector, object_detector,
@@ -379,26 +383,36 @@ def track_camera(
logger.info(f"{name}: exiting subprocess") logger.info(f"{name}: exiting subprocess")
def reduce_boxes(boxes): def box_overlaps(b1, b2):
if len(boxes) == 0: if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
return [] return False
reduced_boxes = cv2.groupRectangles( return True
[list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2
)[0]
return [tuple(b) for b in reduced_boxes] def reduce_boxes(boxes, iou_threshold=0.0):
clusters = []
for box in boxes:
matched = 0
for cluster in clusters:
if intersection_over_union(box, cluster) > iou_threshold:
matched = 1
cluster[0] = min(cluster[0], box[0])
cluster[1] = min(cluster[1], box[1])
cluster[2] = max(cluster[2], box[2])
cluster[3] = max(cluster[3], box[3])
if not matched:
clusters.append(list(box))
return [tuple(c) for c in clusters]
# modified from https://stackoverflow.com/a/40795835
def intersects_any(box_a, boxes): def intersects_any(box_a, boxes):
for box in boxes: for box in boxes:
if ( if box_overlaps(box_a, box):
box_a[2] < box[0] return True
or box_a[0] > box[2] return False
or box_a[1] > box[3]
or box_a[3] < box[1]
):
continue
return True
def detect( def detect(
@@ -434,6 +448,7 @@ def process_frames(
frame_queue: mp.Queue, frame_queue: mp.Queue,
frame_shape, frame_shape,
model_shape, model_shape,
detect_config: DetectConfig,
frame_manager: FrameManager, frame_manager: FrameManager,
motion_detector: MotionDetector, motion_detector: MotionDetector,
object_detector: RemoteObjectDetector, object_detector: RemoteObjectDetector,
@@ -487,11 +502,28 @@ def process_frames(
# look for motion # look for motion
motion_boxes = motion_detector.detect(frame) motion_boxes = motion_detector.detect(frame)
# only get the tracked object boxes that intersect with motion # get stationary object ids
# check every Nth frame for stationary objects
# disappeared objects are not stationary
# also check for overlapping motion boxes
stationary_object_ids = [
obj["id"]
for obj in object_tracker.tracked_objects.values()
# if there hasn't been motion for 10 frames
if obj["motionless_count"] >= 10
# and it isn't due for a periodic check
and obj["motionless_count"] % detect_config.stationary_interval != 0
# and it hasn't disappeared
and object_tracker.disappeared[obj["id"]] == 0
# and it doesn't overlap with any current motion boxes
and not intersects_any(obj["box"], motion_boxes)
]
# get tracked object boxes that aren't stationary
tracked_object_boxes = [ tracked_object_boxes = [
obj["box"] obj["box"]
for obj in object_tracker.tracked_objects.values() for obj in object_tracker.tracked_objects.values()
if intersects_any(obj["box"], motion_boxes) if not obj["id"] in stationary_object_ids
] ]
# combine motion boxes with known locations of existing objects # combine motion boxes with known locations of existing objects
@@ -503,17 +535,25 @@ def process_frames(
for a in combined_boxes for a in combined_boxes
] ]
# combine overlapping regions # consolidate regions with heavy overlap
combined_regions = reduce_boxes(regions)
# re-compute regions
regions = [ regions = [
calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0) calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
for a in combined_regions for a in reduce_boxes(regions, 0.4)
] ]
# resize regions and detect # resize regions and detect
detections = [] # seed with stationary objects
detections = [
(
obj["label"],
obj["score"],
obj["box"],
obj["area"],
obj["region"],
)
for obj in object_tracker.tracked_objects.values()
if obj["id"] in stationary_object_ids
]
for region in regions: for region in regions:
detections.extend( detections.extend(
detect( detect(
@@ -582,14 +622,46 @@ def process_frames(
if refining: if refining:
refine_count += 1 refine_count += 1
# Limit to the detections overlapping with motion areas ## drop detections that overlap too much
# to avoid picking up stationary background objects consolidated_detections = []
detections_with_motion = [ # group by name
d for d in detections if intersects_any(d[2], motion_boxes) detected_object_groups = defaultdict(lambda: [])
] for detection in detections:
detected_object_groups[detection[0]].append(detection)
# loop over detections grouped by label
for group in detected_object_groups.values():
# if the group only has 1 item, skip
if len(group) == 1:
consolidated_detections.append(group[0])
continue
# sort smallest to largest by area
sorted_by_area = sorted(group, key=lambda g: g[3])
for current_detection_idx in range(0, len(sorted_by_area)):
current_detection = sorted_by_area[current_detection_idx][2]
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
len(sorted_by_area),
):
to_check = sorted_by_area[to_check_idx][2]
# if 90% of smaller detection is inside of another detection, consolidate
if (
area(intersection(current_detection, to_check))
/ area(current_detection)
> 0.9
):
overlap = 1
break
if overlap == 0:
consolidated_detections.append(
sorted_by_area[current_detection_idx]
)
# now that we have refined our detections, we need to track objects # now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, detections_with_motion) object_tracker.match_and_update(frame_time, consolidated_detections)
# add to the queue if not full # add to the queue if not full
if detected_objects_queue.full(): if detected_objects_queue.full():

View File

@@ -0,0 +1,43 @@
"""Peewee migrations -- 004_add_bbox_region_area.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 playhouse.sqlite_ext import *
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.drop_not_null(Event, "end_time")
def rollback(migrator, database, fake=False, **kwargs):
pass

View File

@@ -1,23 +1,26 @@
import datetime import sys
from typing_extensions import runtime
sys.path.append("/lab/frigate")
import json import json
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import subprocess as sp import subprocess as sp
import sys import sys
from unittest import TestCase, main
import click import click
import csv
import cv2 import cv2
import numpy as np import numpy as np
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig from frigate.config import FrigateConfig
from frigate.edgetpu import LocalObjectDetector from frigate.edgetpu import LocalObjectDetector
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
from frigate.object_processing import CameraState from frigate.object_processing import CameraState
from frigate.objects import ObjectTracker from frigate.objects import ObjectTracker
from frigate.util import ( from frigate.util import (
DictFrameManager,
EventsPerSecond, EventsPerSecond,
SharedMemoryFrameManager, SharedMemoryFrameManager,
draw_box_with_label, draw_box_with_label,
@@ -96,20 +99,22 @@ class ProcessClip:
ffmpeg_process.wait() ffmpeg_process.wait()
ffmpeg_process.communicate() ffmpeg_process.communicate()
def process_frames(self, objects_to_track=["person"], object_filters={}): def process_frames(
self, object_detector, objects_to_track=["person"], object_filters={}
):
mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8) mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
mask[:] = 255 mask[:] = 255
motion_detector = MotionDetector( motion_detector = MotionDetector(self.frame_shape, self.camera_config.motion)
self.frame_shape, mask, self.camera_config.motion motion_detector.save_images = False
)
object_detector = LocalObjectDetector(labels="/labelmap.txt")
object_tracker = ObjectTracker(self.camera_config.detect) object_tracker = ObjectTracker(self.camera_config.detect)
process_info = { process_info = {
"process_fps": mp.Value("d", 0.0), "process_fps": mp.Value("d", 0.0),
"detection_fps": mp.Value("d", 0.0), "detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0), "detection_frame": mp.Value("d", 0.0),
} }
detection_enabled = mp.Value("d", 1)
stop_event = mp.Event() stop_event = mp.Event()
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
@@ -118,6 +123,7 @@ class ProcessClip:
self.frame_queue, self.frame_queue,
self.frame_shape, self.frame_shape,
model_shape, model_shape,
self.camera_config.detect,
self.frame_manager, self.frame_manager,
motion_detector, motion_detector,
object_detector, object_detector,
@@ -126,25 +132,16 @@ class ProcessClip:
process_info, process_info,
objects_to_track, objects_to_track,
object_filters, object_filters,
mask, detection_enabled,
stop_event, stop_event,
exit_on_empty=True, exit_on_empty=True,
) )
def top_object(self, debug_path=None): def stats(self, debug_path=None):
obj_detected = False total_regions = 0
top_computed_score = 0.0 total_motion_boxes = 0
object_ids = set()
def handle_event(name, obj, frame_time): total_frames = 0
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(): while not self.detected_objects_queue.empty():
( (
@@ -154,7 +151,8 @@ class ProcessClip:
motion_boxes, motion_boxes,
regions, regions,
) = self.detected_objects_queue.get() ) = self.detected_objects_queue.get()
if not debug_path is None:
if debug_path:
self.save_debug_frame( self.save_debug_frame(
debug_path, frame_time, current_tracked_objects.values() debug_path, frame_time, current_tracked_objects.values()
) )
@@ -162,10 +160,22 @@ class ProcessClip:
self.camera_state.update( self.camera_state.update(
frame_time, current_tracked_objects, motion_boxes, regions frame_time, current_tracked_objects, motion_boxes, regions
) )
total_regions += len(regions)
total_motion_boxes += len(motion_boxes)
for id, obj in self.camera_state.tracked_objects.items():
if not obj.false_positive:
object_ids.add(id)
self.frame_manager.delete(self.camera_state.previous_frame_id) total_frames += 1
return {"object_detected": obj_detected, "top_score": top_computed_score} self.frame_manager.delete(self.camera_state.previous_frame_id)
return {
"total_regions": total_regions,
"total_motion_boxes": total_motion_boxes,
"true_positive_objects": len(object_ids),
"total_frames": total_frames,
}
def save_debug_frame(self, debug_path, frame_time, tracked_objects): def save_debug_frame(self, debug_path, frame_time, tracked_objects):
current_frame = cv2.cvtColor( current_frame = cv2.cvtColor(
@@ -178,7 +188,6 @@ class ProcessClip:
for obj in tracked_objects: for obj in tracked_objects:
thickness = 2 thickness = 2
color = (0, 0, 175) color = (0, 0, 175)
if obj["frame_time"] != frame_time: if obj["frame_time"] != frame_time:
thickness = 1 thickness = 1
color = (255, 0, 0) color = (255, 0, 0)
@@ -221,10 +230,9 @@ class ProcessClip:
@click.command() @click.command()
@click.option("-p", "--path", required=True, help="Path to clip or directory to test.") @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("-l", "--label", default="person", help="Label name to detect.")
@click.option("-t", "--threshold", default=0.85, help="Threshold value for objects.") @click.option("-o", "--output", default=None, help="File to save csv of data")
@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.") @click.option("--debug-path", default=None, help="Path to output frames for debugging.")
def process(path, label, threshold, scores, debug_path): def process(path, label, output, debug_path):
clips = [] clips = []
if os.path.isdir(path): if os.path.isdir(path):
files = os.listdir(path) files = os.listdir(path)
@@ -235,51 +243,78 @@ def process(path, label, threshold, scores, debug_path):
json_config = { json_config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
"detectors": {"coral": {"type": "edgetpu", "device": "usb"}},
"cameras": { "cameras": {
"camera": { "camera": {
"ffmpeg": { "ffmpeg": {
"inputs": [ "inputs": [
{ {
"path": "path.mp4", "path": "path.mp4",
"global_args": "", "global_args": "-hide_banner",
"input_args": "", "input_args": "-loglevel info",
"roles": ["detect"], "roles": ["detect"],
} }
] ]
}, },
"height": 1920, "rtmp": {"enabled": False},
"width": 1080, "record": {"enabled": False},
} }
}, },
} }
object_detector = LocalObjectDetector(labels="/labelmap.txt")
results = [] results = []
for c in clips: for c in clips:
logger.info(c) logger.info(c)
frame_shape = get_frame_shape(c) frame_shape = get_frame_shape(c)
json_config["cameras"]["camera"]["height"] = frame_shape[0] json_config["cameras"]["camera"]["detect"] = {
json_config["cameras"]["camera"]["width"] = frame_shape[1] "height": frame_shape[0],
"width": frame_shape[1],
}
json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c
config = FrigateConfig(config=FRIGATE_CONFIG_SCHEMA(json_config)) frigate_config = FrigateConfig(**json_config)
runtime_config = frigate_config.runtime_config
process_clip = ProcessClip(c, frame_shape, config) process_clip = ProcessClip(c, frame_shape, runtime_config)
process_clip.load_frames() process_clip.load_frames()
process_clip.process_frames(objects_to_track=[label]) process_clip.process_frames(object_detector, objects_to_track=[label])
results.append((c, process_clip.top_object(debug_path))) results.append((c, process_clip.stats(debug_path)))
if not scores is None: positive_count = sum(
with open(scores, "w") as writer: 1 for result in results if result[1]["true_positive_objects"] > 0
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( print(
f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s)." f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s)."
) )
if output:
# now we will open a file for writing
data_file = open(output, "w")
# create the csv writer object
csv_writer = csv.writer(data_file)
# Counter variable used for writing
# headers to the CSV file
count = 0
for result in results:
if count == 0:
# Writing headers of CSV file
header = ["file"] + list(result[1].keys())
csv_writer.writerow(header)
count += 1
# Writing data of CSV file
csv_writer.writerow([result[0]] + list(result[1].values()))
data_file.close()
if __name__ == "__main__": if __name__ == "__main__":
process() process()

769
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"@cycjimmy/jsmpeg-player": "^5.0.1", "@cycjimmy/jsmpeg-player": "^5.0.1",
"date-fns": "^2.21.3", "date-fns": "^2.21.3",
"idb-keyval": "^5.0.2", "idb-keyval": "^5.0.2",
"immer": "^8.0.1", "immer": "^9.0.6",
"preact": "^10.5.9", "preact": "^10.5.9",
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -121,12 +121,12 @@ describe('MqttProvider', () => {
</MqttProvider> </MqttProvider>
); );
await screen.findByTestId('data'); await screen.findByTestId('data');
expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}'); expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}');
expect(screen.getByTestId('front/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('front/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}'); expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}');
expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('side/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
}); });
}); });

View File

@@ -42,9 +42,9 @@ export function MqttProvider({
useEffect(() => { useEffect(() => {
Object.keys(config.cameras).forEach((camera) => { Object.keys(config.cameras).forEach((camera) => {
const { name, record, detect, snapshots } = config.cameras[camera]; const { name, record, detect, snapshots } = config.cameras[camera];
dispatch({ topic: `${name}/recordings/state`, payload: record.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/recordings/state`, payload: record.enabled ? 'ON' : 'OFF', retain: true });
dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF', retain: true });
dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF', retain: true });
}); });
}, [config]); }, [config]);

View File

@@ -3,7 +3,7 @@ import { baseUrl } from '../api/baseUrl';
import { useRef, useEffect } from 'preact/hooks'; import { useRef, useEffect } from 'preact/hooks';
import JSMpeg from '@cycjimmy/jsmpeg-player'; import JSMpeg from '@cycjimmy/jsmpeg-player';
export default function JSMpegPlayer({ camera }) { export default function JSMpegPlayer({ camera, width, height }) {
const playerRef = useRef(); const playerRef = useRef();
const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}` const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`
@@ -32,6 +32,6 @@ export default function JSMpegPlayer({ camera }) {
}, [url]); }, [url]);
return ( return (
<div ref={playerRef} class="jsmpeg" /> <div ref={playerRef} class="jsmpeg" style={`max-height: ${height}px; max-width: ${width}px`} />
); );
} }

View File

@@ -21,6 +21,7 @@ export default function Camera({ camera }) {
const [viewMode, setViewMode] = useState('live'); const [viewMode, setViewMode] = useState('live');
const cameraConfig = config?.cameras[camera]; const cameraConfig = config?.cameras[camera];
const liveWidth = Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height))
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject); const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
const handleSetOption = useCallback( const handleSetOption = useCallback(
@@ -87,7 +88,7 @@ export default function Camera({ camera }) {
player = ( player = (
<Fragment> <Fragment>
<div> <div>
<JSMpegPlayer camera={camera} /> <JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
</div> </div>
</Fragment> </Fragment>
); );

View File

@@ -99,7 +99,7 @@ export default function Event({ eventId, close, scrollRef }) {
} }
const startime = new Date(data.start_time * 1000); const startime = new Date(data.start_time * 1000);
const endtime = new Date(data.end_time * 1000); const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex md:flex-row justify-between flex-wrap flex-col"> <div className="flex md:flex-row justify-between flex-wrap flex-col">
@@ -155,7 +155,7 @@ export default function Event({ eventId, close, scrollRef }) {
<Tr index={1}> <Tr index={1}>
<Td>Timeframe</Td> <Td>Timeframe</Td>
<Td> <Td>
{startime.toLocaleString()} {endtime.toLocaleString()} {startime.toLocaleString()}{endtime === null ? ` ${endtime.toLocaleString()}`:''}
</Td> </Td>
</Tr> </Tr>
<Tr> <Tr>
@@ -186,7 +186,7 @@ export default function Event({ eventId, close, scrollRef }) {
}, },
], ],
poster: data.has_snapshot poster: data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg` ? `${apiHost}/api/events/${eventId}/snapshot.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`, : `data:image/jpeg;base64,${data.thumbnail}`,
}} }}
seekOptions={{ forward: 10, back: 5 }} seekOptions={{ forward: 10, back: 5 }}

View File

@@ -42,7 +42,7 @@ const EventsRow = memo(
); );
const start = new Date(parseInt(startTime * 1000, 10)); const start = new Date(parseInt(startTime * 1000, 10));
const end = new Date(parseInt(endTime * 1000, 10)); const end = endTime ? new Date(parseInt(endTime * 1000, 10)) : null;
return ( return (
<Tbody reference={innerRef}> <Tbody reference={innerRef}>
@@ -102,7 +102,7 @@ const EventsRow = memo(
</Td> </Td>
<Td>{start.toLocaleDateString()}</Td> <Td>{start.toLocaleDateString()}</Td>
<Td>{start.toLocaleTimeString()}</Td> <Td>{start.toLocaleTimeString()}</Td>
<Td>{end.toLocaleTimeString()}</Td> <Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>
</Tr> </Tr>
{viewEvent === id ? ( {viewEvent === id ? (
<Tr className="border-b-1"> <Tr className="border-b-1">

View File

@@ -13,7 +13,7 @@ describe('Camera Route', () => {
mockSetOptions = jest.fn(); mockSetOptions = jest.fn();
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]); mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]);
jest.spyOn(Api, 'useConfig').mockImplementation(() => ({ jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
data: { cameras: { front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } } } }, data: { cameras: { front: { name: 'front', detect: {width: 1280, height: 720}, live: {height: 720}, objects: { track: ['taco', 'cat', 'dog'] } } } },
})); }));
jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000'); jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => { jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => {