mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 21:17:43 +03:00
Merge branch 'dev' of https://github.com/ZhaiSoul/frigate into dev
This commit is contained in:
commit
fc43e2ac75
@ -55,7 +55,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \
|
|||||||
FROM scratch AS go2rtc
|
FROM scratch AS go2rtc
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
WORKDIR /rootfs/usr/local/go2rtc/bin
|
WORKDIR /rootfs/usr/local/go2rtc/bin
|
||||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.2/go2rtc_linux_${TARGETARCH}" go2rtc
|
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||||
|
|
||||||
FROM wget AS tempio
|
FROM wget AS tempio
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|||||||
@ -69,10 +69,6 @@ elif go2rtc_config["log"].get("format") is None:
|
|||||||
if go2rtc_config.get("webrtc") is None:
|
if go2rtc_config.get("webrtc") is None:
|
||||||
go2rtc_config["webrtc"] = {}
|
go2rtc_config["webrtc"] = {}
|
||||||
|
|
||||||
# go2rtc should listen on 8555 tcp & udp by default
|
|
||||||
if go2rtc_config["webrtc"].get("listen") is None:
|
|
||||||
go2rtc_config["webrtc"]["listen"] = ":8555"
|
|
||||||
|
|
||||||
if go2rtc_config["webrtc"].get("candidates") is None:
|
if go2rtc_config["webrtc"].get("candidates") is None:
|
||||||
default_candidates = []
|
default_candidates = []
|
||||||
# use internal candidate if it was discovered when running through the add-on
|
# use internal candidate if it was discovered when running through the add-on
|
||||||
@ -84,33 +80,15 @@ if go2rtc_config["webrtc"].get("candidates") is None:
|
|||||||
|
|
||||||
go2rtc_config["webrtc"]["candidates"] = default_candidates
|
go2rtc_config["webrtc"]["candidates"] = default_candidates
|
||||||
|
|
||||||
# This prevents WebRTC from attempting to establish a connection to the internal
|
if go2rtc_config.get("rtsp", {}).get("username") is not None:
|
||||||
# docker IPs which are not accessible from outside the container itself and just
|
go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format(
|
||||||
# wastes time during negotiation. Note that this is only necessary because
|
**FRIGATE_ENV_VARS
|
||||||
# Frigate container doesn't run in host network mode.
|
)
|
||||||
if go2rtc_config["webrtc"].get("filter") is None:
|
|
||||||
go2rtc_config["webrtc"]["filter"] = {"candidates": []}
|
|
||||||
elif go2rtc_config["webrtc"]["filter"].get("candidates") is None:
|
|
||||||
go2rtc_config["webrtc"]["filter"]["candidates"] = []
|
|
||||||
|
|
||||||
# sets default RTSP response to be equivalent to ?video=h264,h265&audio=aac
|
if go2rtc_config.get("rtsp", {}).get("password") is not None:
|
||||||
# this means user does not need to specify audio codec when using restream
|
go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format(
|
||||||
# as source for frigate and the integration supports HLS playback
|
**FRIGATE_ENV_VARS
|
||||||
if go2rtc_config.get("rtsp") is None:
|
)
|
||||||
go2rtc_config["rtsp"] = {"default_query": "mp4"}
|
|
||||||
else:
|
|
||||||
if go2rtc_config["rtsp"].get("default_query") is None:
|
|
||||||
go2rtc_config["rtsp"]["default_query"] = "mp4"
|
|
||||||
|
|
||||||
if go2rtc_config["rtsp"].get("username") is not None:
|
|
||||||
go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format(
|
|
||||||
**FRIGATE_ENV_VARS
|
|
||||||
)
|
|
||||||
|
|
||||||
if go2rtc_config["rtsp"].get("password") is not None:
|
|
||||||
go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format(
|
|
||||||
**FRIGATE_ENV_VARS
|
|
||||||
)
|
|
||||||
|
|
||||||
# ensure ffmpeg path is set correctly
|
# ensure ffmpeg path is set correctly
|
||||||
path = config.get("ffmpeg", {}).get("path", "default")
|
path = config.get("ffmpeg", {}).get("path", "default")
|
||||||
|
|||||||
@ -186,7 +186,7 @@ To do this:
|
|||||||
|
|
||||||
### Custom go2rtc version
|
### Custom go2rtc version
|
||||||
|
|
||||||
Frigate currently includes go2rtc v1.9.2, there may be certain cases where you want to run a different version of go2rtc.
|
Frigate currently includes go2rtc v1.9.9, there may be certain cases where you want to run a different version of go2rtc.
|
||||||
|
|
||||||
To do this:
|
To do this:
|
||||||
|
|
||||||
|
|||||||
@ -219,7 +219,7 @@ go2rtc:
|
|||||||
- rtspx://192.168.1.1:7441/abcdefghijk
|
- rtspx://192.168.1.1:7441/abcdefghijk
|
||||||
```
|
```
|
||||||
|
|
||||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-rtsp)
|
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-rtsp)
|
||||||
|
|
||||||
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect.
|
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect.
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,16 @@ id: license_plate_recognition
|
|||||||
title: License Plate Recognition (LPR)
|
title: License Plate Recognition (LPR)
|
||||||
---
|
---
|
||||||
|
|
||||||
Frigate can recognize license plates on vehicles and automatically add the detected characters or recognized name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
|
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
|
||||||
|
|
||||||
LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles.
|
LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles.
|
||||||
|
|
||||||
When a plate is recognized, the detected characters or recognized name is:
|
When a plate is recognized, the recognized name is:
|
||||||
|
|
||||||
- Added as a `sub_label` to the `car` tracked object.
|
- Added to the `car` tracked object as a `sub_label` (if known) or the `recognized_license_plate` field (if unknown)
|
||||||
- Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore.
|
- Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore.
|
||||||
- Filterable through the More Filters menu in Explore.
|
- Filterable through the More Filters menu in Explore.
|
||||||
- Published via the `frigate/events` MQTT topic as a `sub_label` for the tracked object.
|
- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the tracked object.
|
||||||
|
|
||||||
## Model Requirements
|
## Model Requirements
|
||||||
|
|
||||||
@ -71,6 +71,7 @@ Fine-tune the LPR feature using these optional parameters:
|
|||||||
|
|
||||||
- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value.
|
- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value.
|
||||||
- These labels appear in the UI, filters, and notifications.
|
- These labels appear in the UI, filters, and notifications.
|
||||||
|
- Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`.
|
||||||
- **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate.
|
- **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate.
|
||||||
- For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`.
|
- For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`.
|
||||||
- This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`.
|
- This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`.
|
||||||
|
|||||||
@ -591,7 +591,7 @@ genai:
|
|||||||
person: "My special person prompt."
|
person: "My special person prompt."
|
||||||
|
|
||||||
# Optional: Restream configuration
|
# Optional: Restream configuration
|
||||||
# Uses https://github.com/AlexxIT/go2rtc (v1.9.2)
|
# Uses https://github.com/AlexxIT/go2rtc (v1.9.9)
|
||||||
# NOTE: The default go2rtc API port (1984) must be used,
|
# NOTE: The default go2rtc API port (1984) must be used,
|
||||||
# changing this port for the integrated go2rtc instance is not supported.
|
# changing this port for the integrated go2rtc instance is not supported.
|
||||||
go2rtc:
|
go2rtc:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ title: Restream
|
|||||||
|
|
||||||
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||||
|
|
||||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.2) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration) for more advanced configurations and features.
|
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.9) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration) for more advanced configurations and features.
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ cameras:
|
|||||||
|
|
||||||
## Handling Complex Passwords
|
## Handling Complex Passwords
|
||||||
|
|
||||||
go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose.
|
go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ See [this comment(https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-224
|
|||||||
|
|
||||||
## Advanced Restream Configurations
|
## Advanced Restream Configurations
|
||||||
|
|
||||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
||||||
|
|
||||||
NOTE: The output will need to be passed with two curly braces `{{output}}`
|
NOTE: The output will need to be passed with two curly braces `{{output}}`
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect
|
|||||||
|
|
||||||
# Setup a go2rtc stream
|
# Setup a go2rtc stream
|
||||||
|
|
||||||
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#module-streams), not just rtsp.
|
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#module-streams), not just rtsp.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
@ -32,69 +32,74 @@ go2rtc:
|
|||||||
|
|
||||||
After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
|
After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
|
||||||
|
|
||||||
|
|
||||||
### What if my video doesn't play?
|
### What if my video doesn't play?
|
||||||
|
|
||||||
- Check Logs:
|
- Check Logs:
|
||||||
- Access the go2rtc logs in the Frigate UI under Logs in the sidebar.
|
|
||||||
- If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log.
|
- Access the go2rtc logs in the Frigate UI under Logs in the sidebar.
|
||||||
|
- If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log.
|
||||||
|
|
||||||
- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
|
- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
|
||||||
- Navigate to port 1984 in your browser to access go2rtc's web interface.
|
|
||||||
- If using Frigate through Home Assistant, enable the web interface at port 1984.
|
- Navigate to port 1984 in your browser to access go2rtc's web interface.
|
||||||
- If using Docker, forward port 1984 before accessing the web interface.
|
- If using Frigate through Home Assistant, enable the web interface at port 1984.
|
||||||
- Click `stream` for the specific camera to see if the camera's stream is being received.
|
- If using Docker, forward port 1984 before accessing the web interface.
|
||||||
|
- Click `stream` for the specific camera to see if the camera's stream is being received.
|
||||||
|
|
||||||
- Check Video Codec:
|
- Check Video Codec:
|
||||||
- If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
|
|
||||||
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#codecs-madness) in go2rtc documentation.
|
|
||||||
- If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.2#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view.
|
|
||||||
```yaml
|
|
||||||
go2rtc:
|
|
||||||
streams:
|
|
||||||
back:
|
|
||||||
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
|
||||||
- "ffmpeg:back#video=h264#hardware"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Switch to FFmpeg if needed:
|
- If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
|
||||||
- Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
|
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#codecs-madness) in go2rtc documentation.
|
||||||
```yaml
|
- If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.9#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view.
|
||||||
go2rtc:
|
```yaml
|
||||||
streams:
|
go2rtc:
|
||||||
back:
|
streams:
|
||||||
- ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
back:
|
||||||
```
|
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||||
|
- "ffmpeg:back#video=h264#hardware"
|
||||||
|
```
|
||||||
|
|
||||||
- If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC.
|
- Switch to FFmpeg if needed:
|
||||||
- If possible, update your camera's audio settings to AAC in your camera's firmware.
|
|
||||||
- If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows:
|
|
||||||
```yaml
|
|
||||||
go2rtc:
|
|
||||||
streams:
|
|
||||||
back:
|
|
||||||
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
|
||||||
- "ffmpeg:back#audio=aac"
|
|
||||||
```
|
|
||||||
|
|
||||||
If you need to convert **both** the audio and video streams, you can use the following:
|
- Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
back:
|
back:
|
||||||
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
- ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||||
- "ffmpeg:back#video=h264#audio=aac#hardware"
|
```
|
||||||
```
|
|
||||||
|
|
||||||
When using the ffmpeg module, you would add AAC audio like this:
|
- If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC.
|
||||||
|
- If possible, update your camera's audio settings to AAC in your camera's firmware.
|
||||||
|
- If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
back:
|
back:
|
||||||
- "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware"
|
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||||
```
|
- "ffmpeg:back#audio=aac"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to convert **both** the audio and video streams, you can use the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
go2rtc:
|
||||||
|
streams:
|
||||||
|
back:
|
||||||
|
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||||
|
- "ffmpeg:back#video=h264#audio=aac#hardware"
|
||||||
|
```
|
||||||
|
|
||||||
|
When using the ffmpeg module, you would add AAC audio like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
go2rtc:
|
||||||
|
streams:
|
||||||
|
back:
|
||||||
|
- "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware"
|
||||||
|
```
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,9 @@ Message published for each changed tracked object. The first message is publishe
|
|||||||
}, // attributes with top score that have been identified on the object at any point
|
}, // attributes with top score that have been identified on the object at any point
|
||||||
"current_attributes": [], // detailed data about the current attributes in this frame
|
"current_attributes": [], // detailed data about the current attributes in this frame
|
||||||
"current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
|
"current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
|
||||||
"velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
|
"velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
|
||||||
|
"recognized_license_plate": "ABC12345", // a recognized license plate for car objects
|
||||||
|
"recognized_license_plate_score": 0.933451
|
||||||
},
|
},
|
||||||
"after": {
|
"after": {
|
||||||
"id": "1607123955.475377-mxklsc",
|
"id": "1607123955.475377-mxklsc",
|
||||||
@ -93,7 +95,9 @@ Message published for each changed tracked object. The first message is publishe
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
|
"current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
|
||||||
"velocity_angle": 180 // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
|
"velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
|
||||||
|
"recognized_license_plate": "ABC12345", // a recognized license plate for car objects
|
||||||
|
"recognized_license_plate_score": 0.933451
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
138
docs/sidebars.ts
138
docs/sidebars.ts
@ -1,106 +1,106 @@
|
|||||||
import type { SidebarsConfig, } from '@docusaurus/plugin-content-docs';
|
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
||||||
import { PropSidebarItemLink } from '@docusaurus/plugin-content-docs';
|
import { PropSidebarItemLink } from "@docusaurus/plugin-content-docs";
|
||||||
import frigateHttpApiSidebar from './docs/integrations/api/sidebar';
|
import frigateHttpApiSidebar from "./docs/integrations/api/sidebar";
|
||||||
|
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
docs: {
|
docs: {
|
||||||
Frigate: [
|
Frigate: [
|
||||||
'frigate/index',
|
"frigate/index",
|
||||||
'frigate/hardware',
|
"frigate/hardware",
|
||||||
'frigate/installation',
|
"frigate/installation",
|
||||||
'frigate/camera_setup',
|
"frigate/camera_setup",
|
||||||
'frigate/video_pipeline',
|
"frigate/video_pipeline",
|
||||||
'frigate/glossary',
|
"frigate/glossary",
|
||||||
],
|
],
|
||||||
Guides: [
|
Guides: [
|
||||||
'guides/getting_started',
|
"guides/getting_started",
|
||||||
'guides/configuring_go2rtc',
|
"guides/configuring_go2rtc",
|
||||||
'guides/ha_notifications',
|
"guides/ha_notifications",
|
||||||
'guides/ha_network_storage',
|
"guides/ha_network_storage",
|
||||||
'guides/reverse_proxy',
|
"guides/reverse_proxy",
|
||||||
],
|
],
|
||||||
Configuration: {
|
Configuration: {
|
||||||
'Configuration Files': [
|
"Configuration Files": [
|
||||||
'configuration/index',
|
"configuration/index",
|
||||||
'configuration/reference',
|
"configuration/reference",
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: "link",
|
||||||
label: 'Go2RTC Configuration Reference',
|
label: "Go2RTC Configuration Reference",
|
||||||
href: 'https://github.com/AlexxIT/go2rtc/tree/v1.9.2#configuration',
|
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.9#configuration",
|
||||||
} as PropSidebarItemLink,
|
} as PropSidebarItemLink,
|
||||||
],
|
],
|
||||||
Detectors: [
|
Detectors: [
|
||||||
'configuration/object_detectors',
|
"configuration/object_detectors",
|
||||||
'configuration/audio_detectors',
|
"configuration/audio_detectors",
|
||||||
],
|
],
|
||||||
Classifiers: [
|
Classifiers: [
|
||||||
'configuration/semantic_search',
|
"configuration/semantic_search",
|
||||||
'configuration/genai',
|
"configuration/genai",
|
||||||
'configuration/face_recognition',
|
"configuration/face_recognition",
|
||||||
'configuration/license_plate_recognition',
|
"configuration/license_plate_recognition",
|
||||||
],
|
],
|
||||||
Cameras: [
|
Cameras: [
|
||||||
'configuration/cameras',
|
"configuration/cameras",
|
||||||
'configuration/review',
|
"configuration/review",
|
||||||
'configuration/record',
|
"configuration/record",
|
||||||
'configuration/snapshots',
|
"configuration/snapshots",
|
||||||
'configuration/motion_detection',
|
"configuration/motion_detection",
|
||||||
'configuration/birdseye',
|
"configuration/birdseye",
|
||||||
'configuration/live',
|
"configuration/live",
|
||||||
'configuration/restream',
|
"configuration/restream",
|
||||||
'configuration/autotracking',
|
"configuration/autotracking",
|
||||||
'configuration/camera_specific',
|
"configuration/camera_specific",
|
||||||
],
|
],
|
||||||
Objects: [
|
Objects: [
|
||||||
'configuration/object_filters',
|
"configuration/object_filters",
|
||||||
'configuration/masks',
|
"configuration/masks",
|
||||||
'configuration/zones',
|
"configuration/zones",
|
||||||
'configuration/objects',
|
"configuration/objects",
|
||||||
'configuration/stationary_objects',
|
"configuration/stationary_objects",
|
||||||
],
|
],
|
||||||
'Extra Configuration': [
|
"Extra Configuration": [
|
||||||
'configuration/authentication',
|
"configuration/authentication",
|
||||||
'configuration/notifications',
|
"configuration/notifications",
|
||||||
'configuration/hardware_acceleration',
|
"configuration/hardware_acceleration",
|
||||||
'configuration/ffmpeg_presets',
|
"configuration/ffmpeg_presets",
|
||||||
"configuration/pwa",
|
"configuration/pwa",
|
||||||
'configuration/tls',
|
"configuration/tls",
|
||||||
'configuration/advanced',
|
"configuration/advanced",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Integrations: [
|
Integrations: [
|
||||||
'integrations/plus',
|
"integrations/plus",
|
||||||
'integrations/home-assistant',
|
"integrations/home-assistant",
|
||||||
// This is the HTTP API generated by OpenAPI
|
// This is the HTTP API generated by OpenAPI
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: "category",
|
||||||
label: 'HTTP API',
|
label: "HTTP API",
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: "generated-index",
|
||||||
title: 'Frigate HTTP API',
|
title: "Frigate HTTP API",
|
||||||
description: 'HTTP API',
|
description: "HTTP API",
|
||||||
slug: '/integrations/api/frigate-http-api',
|
slug: "/integrations/api/frigate-http-api",
|
||||||
},
|
},
|
||||||
items: frigateHttpApiSidebar,
|
items: frigateHttpApiSidebar,
|
||||||
},
|
},
|
||||||
'integrations/mqtt',
|
"integrations/mqtt",
|
||||||
'configuration/metrics',
|
"configuration/metrics",
|
||||||
'integrations/third_party_extensions',
|
"integrations/third_party_extensions",
|
||||||
],
|
],
|
||||||
'Frigate+': [
|
"Frigate+": [
|
||||||
'plus/index',
|
"plus/index",
|
||||||
'plus/first_model',
|
"plus/first_model",
|
||||||
'plus/improving_model',
|
"plus/improving_model",
|
||||||
'plus/faq',
|
"plus/faq",
|
||||||
],
|
],
|
||||||
Troubleshooting: [
|
Troubleshooting: [
|
||||||
'troubleshooting/faqs',
|
"troubleshooting/faqs",
|
||||||
'troubleshooting/recordings',
|
"troubleshooting/recordings",
|
||||||
'troubleshooting/edgetpu',
|
"troubleshooting/edgetpu",
|
||||||
],
|
],
|
||||||
Development: [
|
Development: [
|
||||||
'development/contributing',
|
"development/contributing",
|
||||||
'development/contributing-boards',
|
"development/contributing-boards",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -619,6 +619,41 @@ def get_sub_labels(split_joined: Optional[int] = None):
|
|||||||
return JSONResponse(content=sub_labels)
|
return JSONResponse(content=sub_labels)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/recognized_license_plates")
|
||||||
|
def get_recognized_license_plates(split_joined: Optional[int] = None):
|
||||||
|
try:
|
||||||
|
events = Event.select(Event.data).distinct()
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse(
|
||||||
|
content=(
|
||||||
|
{"success": False, "message": "Failed to get recognized license plates"}
|
||||||
|
),
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
recognized_license_plates = []
|
||||||
|
for e in events:
|
||||||
|
if e.data is not None and "recognized_license_plate" in e.data:
|
||||||
|
recognized_license_plates.append(e.data["recognized_license_plate"])
|
||||||
|
|
||||||
|
while None in recognized_license_plates:
|
||||||
|
recognized_license_plates.remove(None)
|
||||||
|
|
||||||
|
if split_joined:
|
||||||
|
original_recognized_license_plates = recognized_license_plates.copy()
|
||||||
|
for recognized_license_plate in original_recognized_license_plates:
|
||||||
|
if recognized_license_plate and "," in recognized_license_plate:
|
||||||
|
recognized_license_plates.remove(recognized_license_plate)
|
||||||
|
parts = recognized_license_plate.split(",")
|
||||||
|
for part in parts:
|
||||||
|
if part.strip() not in recognized_license_plates:
|
||||||
|
recognized_license_plates.append(part.strip())
|
||||||
|
|
||||||
|
recognized_license_plates = list(set(recognized_license_plates))
|
||||||
|
recognized_license_plates.sort()
|
||||||
|
return JSONResponse(content=recognized_license_plates)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/timeline")
|
@router.get("/timeline")
|
||||||
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel):
|
|||||||
max_score: Optional[float] = None
|
max_score: Optional[float] = None
|
||||||
min_speed: Optional[float] = None
|
min_speed: Optional[float] = None
|
||||||
max_speed: Optional[float] = None
|
max_speed: Optional[float] = None
|
||||||
|
recognized_license_plate: Optional[str] = "all"
|
||||||
is_submitted: Optional[int] = None
|
is_submitted: Optional[int] = None
|
||||||
min_length: Optional[float] = None
|
min_length: Optional[float] = None
|
||||||
max_length: Optional[float] = None
|
max_length: Optional[float] = None
|
||||||
@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
max_score: Optional[float] = None
|
max_score: Optional[float] = None
|
||||||
min_speed: Optional[float] = None
|
min_speed: Optional[float] = None
|
||||||
max_speed: Optional[float] = None
|
max_speed: Optional[float] = None
|
||||||
|
recognized_license_plate: Optional[str] = "all"
|
||||||
sort: Optional[str] = None
|
sort: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()):
|
|||||||
min_length = params.min_length
|
min_length = params.min_length
|
||||||
max_length = params.max_length
|
max_length = params.max_length
|
||||||
event_id = params.event_id
|
event_id = params.event_id
|
||||||
|
recognized_license_plate = params.recognized_license_plate
|
||||||
|
|
||||||
sort = params.sort
|
sort = params.sort
|
||||||
|
|
||||||
@ -158,6 +159,45 @@ def events(params: EventsQueryParams = Depends()):
|
|||||||
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
||||||
clauses.append((sub_label_clause))
|
clauses.append((sub_label_clause))
|
||||||
|
|
||||||
|
if recognized_license_plate != "all":
|
||||||
|
# use matching so joined recognized_license_plates are included
|
||||||
|
# for example a recognized license plate 'ABC123' would get events
|
||||||
|
# with recognized license plates 'ABC123' and 'ABC123, XYZ789'
|
||||||
|
recognized_license_plate_clauses = []
|
||||||
|
filtered_recognized_license_plates = recognized_license_plate.split(",")
|
||||||
|
|
||||||
|
if "None" in filtered_recognized_license_plates:
|
||||||
|
filtered_recognized_license_plates.remove("None")
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(Event.data["recognized_license_plate"].is_null())
|
||||||
|
)
|
||||||
|
|
||||||
|
for recognized_license_plate in filtered_recognized_license_plates:
|
||||||
|
# Exact matching plus list inclusion
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
== recognized_license_plate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
% f"*{recognized_license_plate},*"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
% f"*, {recognized_license_plate}*"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
recognized_license_plate_clause = reduce(
|
||||||
|
operator.or_, recognized_license_plate_clauses
|
||||||
|
)
|
||||||
|
clauses.append((recognized_license_plate_clause))
|
||||||
|
|
||||||
if zones != "all":
|
if zones != "all":
|
||||||
# use matching so events with multiple zones
|
# use matching so events with multiple zones
|
||||||
# still match on a search where any zone matches
|
# still match on a search where any zone matches
|
||||||
@ -340,6 +380,8 @@ def events_explore(limit: int = 10):
|
|||||||
"average_estimated_speed",
|
"average_estimated_speed",
|
||||||
"velocity_angle",
|
"velocity_angle",
|
||||||
"path_data",
|
"path_data",
|
||||||
|
"recognized_license_plate",
|
||||||
|
"recognized_license_plate_score",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"event_count": label_counts[event.label],
|
"event_count": label_counts[event.label],
|
||||||
@ -397,6 +439,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
has_clip = params.has_clip
|
has_clip = params.has_clip
|
||||||
has_snapshot = params.has_snapshot
|
has_snapshot = params.has_snapshot
|
||||||
is_submitted = params.is_submitted
|
is_submitted = params.is_submitted
|
||||||
|
recognized_license_plate = params.recognized_license_plate
|
||||||
|
|
||||||
# for similarity search
|
# for similarity search
|
||||||
event_id = params.event_id
|
event_id = params.event_id
|
||||||
@ -466,6 +509,45 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
|
|
||||||
event_filters.append((reduce(operator.or_, zone_clauses)))
|
event_filters.append((reduce(operator.or_, zone_clauses)))
|
||||||
|
|
||||||
|
if recognized_license_plate != "all":
|
||||||
|
# use matching so joined recognized_license_plates are included
|
||||||
|
# for example an recognized_license_plate 'ABC123' would get events
|
||||||
|
# with recognized_license_plates 'ABC123' and 'ABC123, XYZ789'
|
||||||
|
recognized_license_plate_clauses = []
|
||||||
|
filtered_recognized_license_plates = recognized_license_plate.split(",")
|
||||||
|
|
||||||
|
if "None" in filtered_recognized_license_plates:
|
||||||
|
filtered_recognized_license_plates.remove("None")
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(Event.data["recognized_license_plate"].is_null())
|
||||||
|
)
|
||||||
|
|
||||||
|
for recognized_license_plate in filtered_recognized_license_plates:
|
||||||
|
# Exact matching plus list inclusion
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
== recognized_license_plate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
% f"*{recognized_license_plate},*"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recognized_license_plate_clauses.append(
|
||||||
|
(
|
||||||
|
Event.data["recognized_license_plate"].cast("text")
|
||||||
|
% f"*, {recognized_license_plate}*"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
recognized_license_plate_clause = reduce(
|
||||||
|
operator.or_, recognized_license_plate_clauses
|
||||||
|
)
|
||||||
|
event_filters.append((recognized_license_plate_clause))
|
||||||
|
|
||||||
if after:
|
if after:
|
||||||
event_filters.append((Event.start_time > after))
|
event_filters.append((Event.start_time > after))
|
||||||
|
|
||||||
@ -627,6 +709,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
"average_estimated_speed",
|
"average_estimated_speed",
|
||||||
"velocity_angle",
|
"velocity_angle",
|
||||||
"path_data",
|
"path_data",
|
||||||
|
"recognized_license_plate",
|
||||||
|
"recognized_license_plate_score",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,6 +765,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
|||||||
Event.camera,
|
Event.camera,
|
||||||
Event.label,
|
Event.label,
|
||||||
Event.sub_label,
|
Event.sub_label,
|
||||||
|
Event.data,
|
||||||
fn.strftime(
|
fn.strftime(
|
||||||
"%Y-%m-%d",
|
"%Y-%m-%d",
|
||||||
fn.datetime(
|
fn.datetime(
|
||||||
@ -695,6 +780,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
|||||||
Event.camera,
|
Event.camera,
|
||||||
Event.label,
|
Event.label,
|
||||||
Event.sub_label,
|
Event.sub_label,
|
||||||
|
Event.data,
|
||||||
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
|
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
|
||||||
Event.zones,
|
Event.zones,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -137,12 +137,16 @@ class CameraState:
|
|||||||
# draw the bounding boxes on the frame
|
# draw the bounding boxes on the frame
|
||||||
box = obj["box"]
|
box = obj["box"]
|
||||||
text = (
|
text = (
|
||||||
obj["label"]
|
obj["sub_label"][0]
|
||||||
if (
|
if (
|
||||||
not obj.get("sub_label")
|
obj.get("sub_label") and is_label_printable(obj["sub_label"][0])
|
||||||
or not is_label_printable(obj["sub_label"][0])
|
|
||||||
)
|
)
|
||||||
else obj["sub_label"][0]
|
else obj.get("recognized_license_plate", [None])[0]
|
||||||
|
if (
|
||||||
|
obj.get("recognized_license_plate")
|
||||||
|
and obj["recognized_license_plate"][0]
|
||||||
|
)
|
||||||
|
else obj["label"]
|
||||||
)
|
)
|
||||||
draw_box_with_label(
|
draw_box_with_label(
|
||||||
frame_copy,
|
frame_copy,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ class EventMetadataTypeEnum(str, Enum):
|
|||||||
manual_event_end = "manual_event_end"
|
manual_event_end = "manual_event_end"
|
||||||
regenerate_description = "regenerate_description"
|
regenerate_description = "regenerate_description"
|
||||||
sub_label = "sub_label"
|
sub_label = "sub_label"
|
||||||
|
recognized_license_plate = "recognized_license_plate"
|
||||||
|
|
||||||
|
|
||||||
class EventMetadataPublisher(Publisher):
|
class EventMetadataPublisher(Publisher):
|
||||||
|
|||||||
@ -21,7 +21,7 @@ __all__ = [
|
|||||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"]
|
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"]
|
||||||
FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic"
|
FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic"
|
||||||
|
|
||||||
RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic"
|
RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic-audio-aac"
|
||||||
DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
||||||
"-threads",
|
"-threads",
|
||||||
"2",
|
"2",
|
||||||
|
|||||||
@ -1054,13 +1054,20 @@ class LicensePlateProcessingMixin:
|
|||||||
for plate in plates
|
for plate in plates
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
top_plate,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send the result to the API
|
# If it's a known plate, publish to sub_label
|
||||||
|
if sub_label is not None:
|
||||||
|
self.sub_label_publisher.publish(
|
||||||
|
EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence)
|
||||||
|
)
|
||||||
|
|
||||||
self.sub_label_publisher.publish(
|
self.sub_label_publisher.publish(
|
||||||
EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence)
|
EventMetadataTypeEnum.recognized_license_plate,
|
||||||
|
(id, top_plate, avg_confidence),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.detected_license_plates[id] = {
|
self.detected_license_plates[id] = {
|
||||||
"plate": top_plate,
|
"plate": top_plate,
|
||||||
"char_confidences": top_char_confidences,
|
"char_confidences": top_char_confidences,
|
||||||
|
|||||||
@ -27,6 +27,8 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool:
|
|||||||
or prev_event["average_estimated_speed"]
|
or prev_event["average_estimated_speed"]
|
||||||
!= current_event["average_estimated_speed"]
|
!= current_event["average_estimated_speed"]
|
||||||
or prev_event["velocity_angle"] != current_event["velocity_angle"]
|
or prev_event["velocity_angle"] != current_event["velocity_angle"]
|
||||||
|
or prev_event["recognized_license_plate"]
|
||||||
|
!= current_event["recognized_license_plate"]
|
||||||
or prev_event["path_data"] != current_event["path_data"]
|
or prev_event["path_data"] != current_event["path_data"]
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@ -226,6 +228,15 @@ class EventProcessor(threading.Thread):
|
|||||||
event[Event.sub_label] = event_data["sub_label"][0]
|
event[Event.sub_label] = event_data["sub_label"][0]
|
||||||
event[Event.data]["sub_label_score"] = event_data["sub_label"][1]
|
event[Event.data]["sub_label_score"] = event_data["sub_label"][1]
|
||||||
|
|
||||||
|
# only overwrite the recognized_license_plate in the database if it's set
|
||||||
|
if event_data.get("recognized_license_plate") is not None:
|
||||||
|
event[Event.data]["recognized_license_plate"] = event_data[
|
||||||
|
"recognized_license_plate"
|
||||||
|
][0]
|
||||||
|
event[Event.data]["recognized_license_plate_score"] = event_data[
|
||||||
|
"recognized_license_plate"
|
||||||
|
][1]
|
||||||
|
|
||||||
(
|
(
|
||||||
Event.insert(event)
|
Event.insert(event)
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
|
|||||||
@ -346,6 +346,44 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_recognized_license_plate(
|
||||||
|
self, event_id: str, recognized_license_plate: str | None, score: float | None
|
||||||
|
) -> None:
|
||||||
|
"""Update recognized license plate for given event id."""
|
||||||
|
tracked_obj: TrackedObject = None
|
||||||
|
|
||||||
|
for state in self.camera_states.values():
|
||||||
|
tracked_obj = state.tracked_objects.get(event_id)
|
||||||
|
|
||||||
|
if tracked_obj is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
event: Event = Event.get(Event.id == event_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
event = None
|
||||||
|
|
||||||
|
if not tracked_obj and not event:
|
||||||
|
return
|
||||||
|
|
||||||
|
if tracked_obj:
|
||||||
|
tracked_obj.obj_data["recognized_license_plate"] = (
|
||||||
|
recognized_license_plate,
|
||||||
|
score,
|
||||||
|
)
|
||||||
|
|
||||||
|
if event:
|
||||||
|
data = event.data
|
||||||
|
data["recognized_license_plate"] = recognized_license_plate
|
||||||
|
if recognized_license_plate is None:
|
||||||
|
data["recognized_license_plate_score"] = None
|
||||||
|
elif score is not None:
|
||||||
|
data["recognized_license_plate_score"] = score
|
||||||
|
event.data = data
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def create_manual_event(self, payload: tuple) -> None:
|
def create_manual_event(self, payload: tuple) -> None:
|
||||||
(
|
(
|
||||||
frame_time,
|
frame_time,
|
||||||
@ -507,6 +545,11 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
if topic.endswith(EventMetadataTypeEnum.sub_label.value):
|
if topic.endswith(EventMetadataTypeEnum.sub_label.value):
|
||||||
(event_id, sub_label, score) = payload
|
(event_id, sub_label, score) = payload
|
||||||
self.set_sub_label(event_id, sub_label, score)
|
self.set_sub_label(event_id, sub_label, score)
|
||||||
|
if topic.endswith(EventMetadataTypeEnum.recognized_license_plate.value):
|
||||||
|
(event_id, recognized_license_plate, score) = payload
|
||||||
|
self.set_recognized_license_plate(
|
||||||
|
event_id, recognized_license_plate, score
|
||||||
|
)
|
||||||
elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value):
|
elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value):
|
||||||
self.create_manual_event(payload)
|
self.create_manual_event(payload)
|
||||||
elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value):
|
elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value):
|
||||||
|
|||||||
@ -153,6 +153,12 @@ class TrackedObject:
|
|||||||
"current_estimated_speed": self.current_estimated_speed,
|
"current_estimated_speed": self.current_estimated_speed,
|
||||||
"velocity_angle": self.velocity_angle,
|
"velocity_angle": self.velocity_angle,
|
||||||
"path_data": self.path_data,
|
"path_data": self.path_data,
|
||||||
|
"recognized_license_plate": obj_data.get(
|
||||||
|
"recognized_license_plate"
|
||||||
|
),
|
||||||
|
"recognized_license_plate_score": obj_data.get(
|
||||||
|
"recognized_license_plate_score"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
thumb_update = True
|
thumb_update = True
|
||||||
|
|
||||||
@ -365,6 +371,7 @@ class TrackedObject:
|
|||||||
"average_estimated_speed": self.average_estimated_speed,
|
"average_estimated_speed": self.average_estimated_speed,
|
||||||
"velocity_angle": self.velocity_angle,
|
"velocity_angle": self.velocity_angle,
|
||||||
"path_data": self.path_data,
|
"path_data": self.path_data,
|
||||||
|
"recognized_license_plate": self.obj_data.get("recognized_license_plate"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|||||||
@ -337,6 +337,23 @@ function ObjectDetailsTab({
|
|||||||
}
|
}
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
|
const recognizedLicensePlateScore = useMemo(() => {
|
||||||
|
if (!search) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
search.data.recognized_license_plate &&
|
||||||
|
search.data?.recognized_license_plate_score
|
||||||
|
) {
|
||||||
|
return Math.round(
|
||||||
|
(search.data?.recognized_license_plate_score ?? 0) * 100,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
const averageEstimatedSpeed = useMemo(() => {
|
const averageEstimatedSpeed = useMemo(() => {
|
||||||
if (!search || !search.data?.average_estimated_speed) {
|
if (!search || !search.data?.average_estimated_speed) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -547,6 +564,20 @@ function ObjectDetailsTab({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{search?.data.recognized_license_plate && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="text-sm text-primary/40">
|
||||||
|
Recognized License Plate
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-0.5 text-sm">
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
{search.data.recognized_license_plate}{" "}
|
||||||
|
{recognizedLicensePlateScore &&
|
||||||
|
` (${recognizedLicensePlateScore}%)`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="text-sm text-primary/40">
|
<div className="text-sm text-primary/40">
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
|
|||||||
@ -34,6 +34,14 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { LuCheck } from "react-icons/lu";
|
||||||
|
|
||||||
type SearchFilterDialogProps = {
|
type SearchFilterDialogProps = {
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
@ -78,7 +86,8 @@ export default function SearchFilterDialog({
|
|||||||
(currentFilter.max_score ?? 1) < 1 ||
|
(currentFilter.max_score ?? 1) < 1 ||
|
||||||
(currentFilter.max_speed ?? 150) < 150 ||
|
(currentFilter.max_speed ?? 150) < 150 ||
|
||||||
(currentFilter.zones?.length ?? 0) > 0 ||
|
(currentFilter.zones?.length ?? 0) > 0 ||
|
||||||
(currentFilter.sub_labels?.length ?? 0) > 0),
|
(currentFilter.sub_labels?.length ?? 0) > 0 ||
|
||||||
|
(currentFilter.recognized_license_plate?.length ?? 0) > 0),
|
||||||
[currentFilter],
|
[currentFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -120,6 +129,15 @@ export default function SearchFilterDialog({
|
|||||||
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<RecognizedLicensePlatesFilterContent
|
||||||
|
recognizedLicensePlates={currentFilter.recognized_license_plate}
|
||||||
|
setRecognizedLicensePlates={(plate) =>
|
||||||
|
setCurrentFilter({
|
||||||
|
...currentFilter,
|
||||||
|
recognized_license_plate: plate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ScoreFilterContent
|
<ScoreFilterContent
|
||||||
minScore={currentFilter.min_score}
|
minScore={currentFilter.min_score}
|
||||||
maxScore={currentFilter.max_score}
|
maxScore={currentFilter.max_score}
|
||||||
@ -193,6 +211,7 @@ export default function SearchFilterDialog({
|
|||||||
max_speed: undefined,
|
max_speed: undefined,
|
||||||
has_snapshot: undefined,
|
has_snapshot: undefined,
|
||||||
has_clip: undefined,
|
has_clip: undefined,
|
||||||
|
recognized_license_plate: undefined,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -843,3 +862,130 @@ export function SnapshotClipFilterContent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecognizedLicensePlatesFilterContentProps = {
|
||||||
|
recognizedLicensePlates: string[] | undefined;
|
||||||
|
setRecognizedLicensePlates: (
|
||||||
|
recognizedLicensePlates: string[] | undefined,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RecognizedLicensePlatesFilterContent({
|
||||||
|
recognizedLicensePlates,
|
||||||
|
setRecognizedLicensePlates,
|
||||||
|
}: RecognizedLicensePlatesFilterContentProps) {
|
||||||
|
const { data: allRecognizedLicensePlates, error } = useSWR<string[]>(
|
||||||
|
"recognized_license_plates",
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedRecognizedLicensePlates, setSelectedRecognizedLicensePlates] =
|
||||||
|
useState<string[]>(recognizedLicensePlates || []);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (recognizedLicensePlates) {
|
||||||
|
setSelectedRecognizedLicensePlates(recognizedLicensePlates);
|
||||||
|
} else {
|
||||||
|
setSelectedRecognizedLicensePlates([]);
|
||||||
|
}
|
||||||
|
}, [recognizedLicensePlates]);
|
||||||
|
|
||||||
|
const handleSelect = (recognizedLicensePlate: string) => {
|
||||||
|
const newSelected = selectedRecognizedLicensePlates.includes(
|
||||||
|
recognizedLicensePlate,
|
||||||
|
)
|
||||||
|
? selectedRecognizedLicensePlates.filter(
|
||||||
|
(id) => id !== recognizedLicensePlate,
|
||||||
|
) // Deselect
|
||||||
|
: [...selectedRecognizedLicensePlates, recognizedLicensePlate]; // Select
|
||||||
|
|
||||||
|
setSelectedRecognizedLicensePlates(newSelected);
|
||||||
|
if (newSelected.length === 0) {
|
||||||
|
setRecognizedLicensePlates(undefined); // Clear filter if no plates selected
|
||||||
|
} else {
|
||||||
|
setRecognizedLicensePlates(newSelected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!allRecognizedLicensePlates || allRecognizedLicensePlates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRecognizedLicensePlates =
|
||||||
|
allRecognizedLicensePlates?.filter((id) =>
|
||||||
|
id.toLowerCase().includes(inputValue.toLowerCase()),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-hidden">
|
||||||
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
|
<div className="mb-3 text-lg">Recognized License Plates</div>
|
||||||
|
{error ? (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
Failed to load recognized license plates.
|
||||||
|
</p>
|
||||||
|
) : !allRecognizedLicensePlates ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Loading recognized license plates...
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Command className="border border-input bg-background">
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Type to search license plates..."
|
||||||
|
value={inputValue}
|
||||||
|
onValueChange={setInputValue}
|
||||||
|
/>
|
||||||
|
<CommandList className="max-h-[200px] overflow-auto">
|
||||||
|
{filteredRecognizedLicensePlates.length === 0 && inputValue && (
|
||||||
|
<CommandEmpty>No license plates found.</CommandEmpty>
|
||||||
|
)}
|
||||||
|
{filteredRecognizedLicensePlates.map((plate) => (
|
||||||
|
<CommandItem
|
||||||
|
key={plate}
|
||||||
|
value={plate}
|
||||||
|
onSelect={() => handleSelect(plate)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<LuCheck
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
selectedRecognizedLicensePlates.includes(plate)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{plate}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
{selectedRecognizedLicensePlates.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{selectedRecognizedLicensePlates.map((id) => (
|
||||||
|
<span
|
||||||
|
key={id}
|
||||||
|
className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white"
|
||||||
|
>
|
||||||
|
{id}
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelect(id)}
|
||||||
|
className="ml-1 text-white hover:text-gray-200"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Select one or more plates from the list.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -108,6 +108,8 @@ export default function Explore() {
|
|||||||
cameras: searchSearchParams["cameras"],
|
cameras: searchSearchParams["cameras"],
|
||||||
labels: searchSearchParams["labels"],
|
labels: searchSearchParams["labels"],
|
||||||
sub_labels: searchSearchParams["sub_labels"],
|
sub_labels: searchSearchParams["sub_labels"],
|
||||||
|
recognized_license_plate:
|
||||||
|
searchSearchParams["recognized_license_plate"],
|
||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
@ -143,6 +145,8 @@ export default function Explore() {
|
|||||||
cameras: searchSearchParams["cameras"],
|
cameras: searchSearchParams["cameras"],
|
||||||
labels: searchSearchParams["labels"],
|
labels: searchSearchParams["labels"],
|
||||||
sub_labels: searchSearchParams["sub_labels"],
|
sub_labels: searchSearchParams["sub_labels"],
|
||||||
|
recognized_license_plate:
|
||||||
|
searchSearchParams["recognized_license_plate"],
|
||||||
zones: searchSearchParams["zones"],
|
zones: searchSearchParams["zones"],
|
||||||
before: searchSearchParams["before"],
|
before: searchSearchParams["before"],
|
||||||
after: searchSearchParams["after"],
|
after: searchSearchParams["after"],
|
||||||
|
|||||||
@ -58,6 +58,8 @@ export type SearchResult = {
|
|||||||
average_estimated_speed: number;
|
average_estimated_speed: number;
|
||||||
velocity_angle: number;
|
velocity_angle: number;
|
||||||
path_data: [number[], number][];
|
path_data: [number[], number][];
|
||||||
|
recognized_license_plate?: string;
|
||||||
|
recognized_license_plate_score?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ export type SearchFilter = {
|
|||||||
cameras?: string[];
|
cameras?: string[];
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
sub_labels?: string[];
|
sub_labels?: string[];
|
||||||
|
recognized_license_plate?: string[];
|
||||||
zones?: string[];
|
zones?: string[];
|
||||||
before?: number;
|
before?: number;
|
||||||
after?: number;
|
after?: number;
|
||||||
@ -89,6 +92,7 @@ export type SearchQueryParams = {
|
|||||||
cameras?: string[];
|
cameras?: string[];
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
sub_labels?: string[];
|
sub_labels?: string[];
|
||||||
|
recognized_license_plate?: string[];
|
||||||
zones?: string[];
|
zones?: string[];
|
||||||
before?: string;
|
before?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
|
|||||||
@ -123,6 +123,9 @@ export default function SearchView({
|
|||||||
}, [config, searchFilter]);
|
}, [config, searchFilter]);
|
||||||
|
|
||||||
const { data: allSubLabels } = useSWR("sub_labels");
|
const { data: allSubLabels } = useSWR("sub_labels");
|
||||||
|
const { data: allRecognizedLicensePlates } = useSWR(
|
||||||
|
"recognized_license_plates",
|
||||||
|
);
|
||||||
|
|
||||||
const allZones = useMemo<string[]>(() => {
|
const allZones = useMemo<string[]>(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -162,12 +165,20 @@ export default function SearchView({
|
|||||||
max_score: ["100"],
|
max_score: ["100"],
|
||||||
min_speed: ["1"],
|
min_speed: ["1"],
|
||||||
max_speed: ["150"],
|
max_speed: ["150"],
|
||||||
|
recognized_license_plate: allRecognizedLicensePlates,
|
||||||
has_clip: ["yes", "no"],
|
has_clip: ["yes", "no"],
|
||||||
has_snapshot: ["yes", "no"],
|
has_snapshot: ["yes", "no"],
|
||||||
...(config?.plus?.enabled &&
|
...(config?.plus?.enabled &&
|
||||||
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
|
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
|
||||||
}),
|
}),
|
||||||
[config, allLabels, allZones, allSubLabels, searchFilter],
|
[
|
||||||
|
config,
|
||||||
|
allLabels,
|
||||||
|
allZones,
|
||||||
|
allSubLabels,
|
||||||
|
allRecognizedLicensePlates,
|
||||||
|
searchFilter,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// remove duplicate event ids
|
// remove duplicate event ids
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user