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

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