mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Compare commits
14 Commits
1aea5b695d
...
e99dddc9c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e99dddc9c9 | ||
|
|
1f9669bbe5 | ||
|
|
acb17a7b50 | ||
|
|
7933a83a42 | ||
|
|
2eef58aa1d | ||
|
|
6659b7cb0f | ||
|
|
f134796913 | ||
|
|
b4abbd7d3b | ||
|
|
438df7d484 | ||
|
|
e27a94ae0b | ||
|
|
1dee548dbc | ||
|
|
91e17e12b7 | ||
|
|
bb45483e9e | ||
|
|
7b4eaf2d10 |
@ -191,6 +191,7 @@ ONVIF
|
|||||||
openai
|
openai
|
||||||
opencv
|
opencv
|
||||||
openvino
|
openvino
|
||||||
|
overfitting
|
||||||
OWASP
|
OWASP
|
||||||
paddleocr
|
paddleocr
|
||||||
paho
|
paho
|
||||||
|
|||||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -225,3 +225,29 @@ jobs:
|
|||||||
sources: |
|
sources: |
|
||||||
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
|
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
|
||||||
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi
|
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi
|
||||||
|
axera_build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
name: AXERA Build
|
||||||
|
needs:
|
||||||
|
- amd64_build
|
||||||
|
- arm64_build
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up QEMU and Buildx
|
||||||
|
id: setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build and push Axera build
|
||||||
|
uses: docker/bake-action@v6
|
||||||
|
with:
|
||||||
|
source: .
|
||||||
|
push: true
|
||||||
|
targets: axcl
|
||||||
|
files: docker/axcl/axcl.hcl
|
||||||
|
set: |
|
||||||
|
axcl.tags=${{ steps.setup.outputs.image-name }}-axcl
|
||||||
|
*.cache-from=type=gha
|
||||||
55
docker/axcl/Dockerfile
Normal file
55
docker/axcl/Dockerfile
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# syntax=docker/dockerfile:1.6
|
||||||
|
|
||||||
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Globally set pip break-system-packages option to avoid having to specify it every time
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
|
|
||||||
|
|
||||||
|
FROM frigate AS frigate-axcl
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES
|
||||||
|
|
||||||
|
# Install axpyengine
|
||||||
|
RUN wget https://github.com/AXERA-TECH/pyaxengine/releases/download/0.1.3.rc1/axengine-0.1.3-py3-none-any.whl -O /axengine-0.1.3-py3-none-any.whl
|
||||||
|
RUN pip3 install -i https://mirrors.aliyun.com/pypi/simple/ /axengine-0.1.3-py3-none-any.whl \
|
||||||
|
&& rm /axengine-0.1.3-py3-none-any.whl
|
||||||
|
|
||||||
|
# Install axcl
|
||||||
|
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
echo "Installing x86_64 version of axcl"; \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb -O /axcl.deb; \
|
||||||
|
else \
|
||||||
|
echo "Installing aarch64 version of axcl"; \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb -O /axcl.deb; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN mkdir /unpack_axcl && \
|
||||||
|
dpkg-deb -x /axcl.deb /unpack_axcl && \
|
||||||
|
cp -R /unpack_axcl/usr/bin/axcl /usr/bin/ && \
|
||||||
|
cp -R /unpack_axcl/usr/lib/axcl /usr/lib/ && \
|
||||||
|
rm -rf /unpack_axcl /axcl.deb
|
||||||
|
|
||||||
|
|
||||||
|
# Install axcl ffmpeg
|
||||||
|
RUN mkdir -p /usr/lib/ffmpeg/axcl
|
||||||
|
|
||||||
|
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffmpeg-x64 -O /usr/lib/ffmpeg/axcl/ffmpeg && \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffprobe-x64 -O /usr/lib/ffmpeg/axcl/ffprobe; \
|
||||||
|
else \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffmpeg-aarch64 -O /usr/lib/ffmpeg/axcl/ffmpeg && \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffprobe-aarch64 -O /usr/lib/ffmpeg/axcl/ffprobe; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN chmod +x /usr/lib/ffmpeg/axcl/ffmpeg /usr/lib/ffmpeg/axcl/ffprobe
|
||||||
|
|
||||||
|
# Set ldconfig path
|
||||||
|
RUN echo "/usr/lib/axcl" > /etc/ld.so.conf.d/ax.conf
|
||||||
|
|
||||||
|
# Set env
|
||||||
|
ENV PATH="$PATH:/usr/bin/axcl"
|
||||||
|
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/axcl"
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "ldconfig && exec /init"]
|
||||||
13
docker/axcl/axcl.hcl
Normal file
13
docker/axcl/axcl.hcl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
target frigate {
|
||||||
|
dockerfile = "docker/main/Dockerfile"
|
||||||
|
platforms = ["linux/amd64", "linux/arm64"]
|
||||||
|
target = "frigate"
|
||||||
|
}
|
||||||
|
|
||||||
|
target axcl {
|
||||||
|
dockerfile = "docker/axcl/Dockerfile"
|
||||||
|
contexts = {
|
||||||
|
frigate = "target:frigate",
|
||||||
|
}
|
||||||
|
platforms = ["linux/amd64", "linux/arm64"]
|
||||||
|
}
|
||||||
15
docker/axcl/axcl.mk
Normal file
15
docker/axcl/axcl.mk
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
BOARDS += axcl
|
||||||
|
|
||||||
|
local-axcl: version
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=frigate:latest-axcl \
|
||||||
|
--load
|
||||||
|
|
||||||
|
build-axcl: version
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-axcl
|
||||||
|
|
||||||
|
push-axcl: build-axcl
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-axcl \
|
||||||
|
--push
|
||||||
83
docker/axcl/user_installation.sh
Executable file
83
docker/axcl/user_installation.sh
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Update package list and install dependencies
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential cmake git wget pciutils kmod udev
|
||||||
|
|
||||||
|
# Check if gcc-12 is needed
|
||||||
|
current_gcc_version=$(gcc --version | head -n1 | awk '{print $NF}')
|
||||||
|
gcc_major_version=$(echo $current_gcc_version | cut -d'.' -f1)
|
||||||
|
|
||||||
|
if [[ $gcc_major_version -lt 12 ]]; then
|
||||||
|
echo "Current GCC version ($current_gcc_version) is lower than 12, installing gcc-12..."
|
||||||
|
sudo apt-get install -y gcc-12
|
||||||
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
||||||
|
echo "GCC-12 installed and set as default"
|
||||||
|
else
|
||||||
|
echo "Current GCC version ($current_gcc_version) is sufficient, skipping GCC installation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine architecture
|
||||||
|
arch=$(uname -m)
|
||||||
|
download_url=""
|
||||||
|
|
||||||
|
if [[ $arch == "x86_64" ]]; then
|
||||||
|
download_url="https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
deb_file="axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
elif [[ $arch == "aarch64" ]]; then
|
||||||
|
download_url="https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
deb_file="axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
else
|
||||||
|
echo "Unsupported architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download AXCL driver
|
||||||
|
echo "Downloading AXCL driver for $arch..."
|
||||||
|
wget "$download_url" -O "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to download AXCL driver"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install AXCL driver
|
||||||
|
echo "Installing AXCL driver..."
|
||||||
|
sudo dpkg -i "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to install AXCL driver, attempting to fix dependencies..."
|
||||||
|
sudo apt-get install -f -y
|
||||||
|
sudo dpkg -i "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "AXCL driver installation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update environment
|
||||||
|
echo "Updating environment..."
|
||||||
|
source /etc/profile
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying AXCL installation..."
|
||||||
|
if command -v axcl-smi &> /dev/null; then
|
||||||
|
echo "AXCL driver detected, checking AI accelerator status..."
|
||||||
|
|
||||||
|
axcl_output=$(axcl-smi 2>&1)
|
||||||
|
axcl_exit_code=$?
|
||||||
|
|
||||||
|
echo "$axcl_output"
|
||||||
|
|
||||||
|
if [ $axcl_exit_code -eq 0 ]; then
|
||||||
|
echo "AXCL driver installation completed successfully!"
|
||||||
|
else
|
||||||
|
echo "AXCL driver installed but no AI accelerator detected or communication failed."
|
||||||
|
echo "Please check if the AI accelerator is properly connected and powered on."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "axcl-smi command not found. AXCL driver installation may have failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@ -168,6 +168,8 @@ Recorded `speech` events will always use a `whisper` model, regardless of the `m
|
|||||||
|
|
||||||
If you hear speech that’s actually important and worth saving/indexing for the future, **just press the transcribe button in Explore** on that specific `speech` event - that keeps things explicit, reliable, and under your control.
|
If you hear speech that’s actually important and worth saving/indexing for the future, **just press the transcribe button in Explore** on that specific `speech` event - that keeps things explicit, reliable, and under your control.
|
||||||
|
|
||||||
|
Other options are being considered for future versions of Frigate to add transcription options that support external `whisper` Docker containers. A single transcription service could then be shared by Frigate and other applications (for example, Home Assistant Voice), and run on more powerful machines when available.
|
||||||
|
|
||||||
2. Why don't you save live transcription text and use that for `speech` events?
|
2. Why don't you save live transcription text and use that for `speech` events?
|
||||||
|
|
||||||
There’s no guarantee that a `speech` event is even created from the exact audio that went through the transcription model. Live transcription and `speech` event creation are **separate, asynchronous processes**. Even when both are correctly configured, trying to align the **precise start and end time of a speech event** with whatever audio the model happened to be processing at that moment is unreliable.
|
There’s no guarantee that a `speech` event is even created from the exact audio that went through the transcription model. Live transcription and `speech` event creation are **separate, asynchronous processes**. Even when both are correctly configured, trying to align the **precise start and end time of a speech event** with whatever audio the model happened to be processing at that moment is unreliable.
|
||||||
|
|||||||
@ -69,4 +69,6 @@ Once all images are assigned, training will begin automatically.
|
|||||||
### Improving the Model
|
### Improving the Model
|
||||||
|
|
||||||
- **Problem framing**: Keep classes visually distinct and state-focused (e.g., `open`, `closed`, `unknown`). Avoid combining object identity with state in a single model unless necessary.
|
- **Problem framing**: Keep classes visually distinct and state-focused (e.g., `open`, `closed`, `unknown`). Avoid combining object identity with state in a single model unless necessary.
|
||||||
- **Data collection**: Use the model’s Recent Classifications tab to gather balanced examples across times of day and weather.
|
- **Data collection**: Use the model's Recent Classifications tab to gather balanced examples across times of day and weather.
|
||||||
|
- **When to train**: Focus on cases where the model is entirely incorrect or flips between states when it should not. There's no need to train additional images when the model is already working consistently.
|
||||||
|
- **Selecting training images**: Images scoring below 100% due to new conditions (e.g., first snow of the year, seasonal changes) or variations (e.g., objects temporarily in view, insects at night) are good candidates for training, as they represent scenarios different from the default state. Training these lower-scoring images that differ from existing training data helps prevent overfitting. Avoid training large quantities of images that look very similar, especially if they already score 100% as this can lead to overfitting.
|
||||||
|
|||||||
@ -49,6 +49,11 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
||||||
|
|
||||||
|
**AXERA** <CommunityBadge />
|
||||||
|
|
||||||
|
- [AXEngine](#axera): axmodels can run on AXERA AI acceleration.
|
||||||
|
|
||||||
|
|
||||||
**For Testing**
|
**For Testing**
|
||||||
|
|
||||||
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
||||||
@ -1438,6 +1443,41 @@ model:
|
|||||||
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## AXERA
|
||||||
|
|
||||||
|
Hardware accelerated object detection is supported on the following SoCs:
|
||||||
|
|
||||||
|
- AX650N
|
||||||
|
- AX8850N
|
||||||
|
|
||||||
|
This implementation uses the [AXera Pulsar2 Toolchain](https://huggingface.co/AXERA-TECH/Pulsar2).
|
||||||
|
|
||||||
|
See the [installation docs](../frigate/installation.md#axera) for information on configuring the AXEngine hardware.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
When configuring the AXEngine detector, you have to specify the model name.
|
||||||
|
|
||||||
|
#### yolov9
|
||||||
|
|
||||||
|
A yolov9 model is provided in the container at /axmodels and is used by this detector type by default.
|
||||||
|
|
||||||
|
Use the model configuration shown below when using the axengine detector with the default axmodel:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
detectors:
|
||||||
|
axengine:
|
||||||
|
type: axengine
|
||||||
|
|
||||||
|
model:
|
||||||
|
path: frigate-yolov9-tiny
|
||||||
|
model_type: yolo-generic
|
||||||
|
width: 320
|
||||||
|
height: 320
|
||||||
|
tensor_format: bgr
|
||||||
|
labelmap_path: /labelmap/coco-80.txt
|
||||||
|
```
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
|
|
||||||
Some model types are not included in Frigate by default.
|
Some model types are not included in Frigate by default.
|
||||||
|
|||||||
@ -710,6 +710,44 @@ audio_transcription:
|
|||||||
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
||||||
language: en
|
language: en
|
||||||
|
|
||||||
|
# Optional: Configuration for classification models
|
||||||
|
classification:
|
||||||
|
# Optional: Configuration for bird classification
|
||||||
|
bird:
|
||||||
|
# Optional: Enable bird classification (default: shown below)
|
||||||
|
enabled: False
|
||||||
|
# Optional: Minimum classification score required to be considered a match (default: shown below)
|
||||||
|
threshold: 0.9
|
||||||
|
custom:
|
||||||
|
# Required: name of the classification model
|
||||||
|
model_name:
|
||||||
|
# Optional: Enable running the model (default: shown below)
|
||||||
|
enabled: True
|
||||||
|
# Optional: Name of classification model (default: shown below)
|
||||||
|
name: None
|
||||||
|
# Optional: Classification score threshold to change the state (default: shown below)
|
||||||
|
threshold: 0.8
|
||||||
|
# Optional: Number of classification attempts to save in the recent classifications tab (default: shown below)
|
||||||
|
# NOTE: Defaults to 200 for object classification and 100 for state classification if not specified
|
||||||
|
save_attempts: None
|
||||||
|
# Optional: Object classification configuration
|
||||||
|
object_config:
|
||||||
|
# Required: Object types to classify
|
||||||
|
objects: [dog]
|
||||||
|
# Optional: Type of classification that is applied (default: shown below)
|
||||||
|
classification_type: sub_label
|
||||||
|
# Optional: State classification configuration
|
||||||
|
state_config:
|
||||||
|
# Required: Cameras to run classification on
|
||||||
|
cameras:
|
||||||
|
camera_name:
|
||||||
|
# Required: Crop of image frame on this camera to run classification on
|
||||||
|
crop: [0, 180, 220, 400]
|
||||||
|
# Optional: If classification should be run when motion is detected in the crop (default: shown below)
|
||||||
|
motion: False
|
||||||
|
# Optional: Interval to run classification on in seconds (default: shown below)
|
||||||
|
interval: None
|
||||||
|
|
||||||
# Optional: Restream configuration
|
# Optional: Restream configuration
|
||||||
# Uses https://github.com/AlexxIT/go2rtc (v1.9.10)
|
# Uses https://github.com/AlexxIT/go2rtc (v1.9.10)
|
||||||
# NOTE: The default go2rtc API port (1984) must be used,
|
# NOTE: The default go2rtc API port (1984) must be used,
|
||||||
|
|||||||
@ -104,6 +104,10 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection.
|
||||||
|
|
||||||
|
**AXERA** <CommunityBadge />
|
||||||
|
|
||||||
|
- [AXEngine](#axera): axera models can run on AXERA NPUs via AXEngine, delivering highly efficient object detection.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
@ -287,6 +291,14 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
|
|||||||
| ssd mobilenet | ~ 25 ms |
|
| ssd mobilenet | ~ 25 ms |
|
||||||
| yolov5m | ~ 118 ms |
|
| yolov5m | ~ 118 ms |
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
- **AXEngine** Default model is **yolov9**
|
||||||
|
|
||||||
|
| Name | AXERA AX650N/AX8850N Inference Time |
|
||||||
|
| ---------------- | ----------------------------------- |
|
||||||
|
| yolov9-tiny | ~ 4 ms |
|
||||||
|
|
||||||
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
||||||
|
|
||||||
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
||||||
|
|||||||
@ -287,6 +287,42 @@ or add these options to your `docker run` command:
|
|||||||
|
|
||||||
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>AXERA accelerators</summary>
|
||||||
|
AXERA accelerators are available in an M.2 form factor, compatible with both Raspberry Pi and Orange Pi. This form factor has also been successfully tested on x86 platforms, making it a versatile choice for various computing environments.
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
Using AXERA accelerators requires the installation of the AXCL driver. We provide a convenient Linux script to complete this installation.
|
||||||
|
|
||||||
|
Follow these steps for installation:
|
||||||
|
|
||||||
|
1. Copy or download [this script](https://github.com/ivanshi1108/assets/releases/download/v0.16.2/user_installation.sh).
|
||||||
|
2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh`
|
||||||
|
3. Run the script with `./user_installation.sh`
|
||||||
|
|
||||||
|
#### Setup
|
||||||
|
|
||||||
|
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
|
||||||
|
|
||||||
|
Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
devices:
|
||||||
|
- /dev/axcl_host
|
||||||
|
- /dev/ax_mmb_dev
|
||||||
|
- /dev/msg_userdev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using `docker run`, add this option to your command `--device /dev/axcl_host --device /dev/ax_mmb_dev --device /dev/msg_userdev`
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
Finally, configure [hardware object detection](/configuration/object_detectors#axera) to complete the setup.
|
||||||
|
</details>
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Running through Docker with Docker Compose is the recommended install method.
|
Running through Docker with Docker Compose is the recommended install method.
|
||||||
|
|||||||
@ -1731,37 +1731,40 @@ def create_trigger_embedding(
|
|||||||
if event.data.get("type") != "object":
|
if event.data.get("type") != "object":
|
||||||
return
|
return
|
||||||
|
|
||||||
if thumbnail := get_event_thumbnail_bytes(event):
|
# Get the thumbnail
|
||||||
cursor = context.db.execute_sql(
|
thumbnail = get_event_thumbnail_bytes(event)
|
||||||
"""
|
|
||||||
SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ?
|
if thumbnail is None:
|
||||||
""",
|
return JSONResponse(
|
||||||
[body.data],
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": f"Failed to get thumbnail for {body.data} for {body.type} trigger",
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
row = cursor.fetchone() if cursor else None
|
# Try to reuse existing embedding from database
|
||||||
|
cursor = context.db.execute_sql(
|
||||||
|
"""
|
||||||
|
SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ?
|
||||||
|
""",
|
||||||
|
[body.data],
|
||||||
|
)
|
||||||
|
|
||||||
if row:
|
row = cursor.fetchone() if cursor else None
|
||||||
query_embedding = row[0]
|
|
||||||
embedding = np.frombuffer(query_embedding, dtype=np.float32)
|
if row:
|
||||||
|
query_embedding = row[0]
|
||||||
|
embedding = np.frombuffer(query_embedding, dtype=np.float32)
|
||||||
else:
|
else:
|
||||||
# Extract valid thumbnail
|
# Generate new embedding
|
||||||
thumbnail = get_event_thumbnail_bytes(event)
|
|
||||||
|
|
||||||
if thumbnail is None:
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"success": False,
|
|
||||||
"message": f"Failed to get thumbnail for {body.data} for {body.type} trigger",
|
|
||||||
},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
embedding = context.generate_image_embedding(
|
embedding = context.generate_image_embedding(
|
||||||
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
|
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not embedding:
|
if embedding is None or (
|
||||||
|
isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0
|
||||||
|
):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -1896,7 +1899,9 @@ def update_trigger_embedding(
|
|||||||
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
|
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not embedding:
|
if embedding is None or (
|
||||||
|
isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0
|
||||||
|
):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|||||||
@ -105,6 +105,11 @@ class CustomClassificationConfig(FrigateBaseModel):
|
|||||||
threshold: float = Field(
|
threshold: float = Field(
|
||||||
default=0.8, title="Classification score threshold to change the state."
|
default=0.8, title="Classification score threshold to change the state."
|
||||||
)
|
)
|
||||||
|
save_attempts: int | None = Field(
|
||||||
|
default=None,
|
||||||
|
title="Number of classification attempts to save in the recent classifications tab. If not specified, defaults to 200 for object classification and 100 for state classification.",
|
||||||
|
ge=0,
|
||||||
|
)
|
||||||
object_config: CustomClassificationObjectConfig | None = Field(default=None)
|
object_config: CustomClassificationObjectConfig | None = Field(default=None)
|
||||||
state_config: CustomClassificationStateConfig | None = Field(default=None)
|
state_config: CustomClassificationStateConfig | None = Field(default=None)
|
||||||
|
|
||||||
|
|||||||
@ -250,6 +250,11 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi):
|
|||||||
if self.interpreter is None:
|
if self.interpreter is None:
|
||||||
# When interpreter is None, always save (score is 0.0, which is < 1.0)
|
# When interpreter is None, always save (score is 0.0, which is < 1.0)
|
||||||
if self._should_save_image(camera, "unknown", 0.0):
|
if self._should_save_image(camera, "unknown", 0.0):
|
||||||
|
save_attempts = (
|
||||||
|
self.model_config.save_attempts
|
||||||
|
if self.model_config.save_attempts is not None
|
||||||
|
else 100
|
||||||
|
)
|
||||||
write_classification_attempt(
|
write_classification_attempt(
|
||||||
self.train_dir,
|
self.train_dir,
|
||||||
cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),
|
cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),
|
||||||
@ -257,6 +262,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi):
|
|||||||
now,
|
now,
|
||||||
"unknown",
|
"unknown",
|
||||||
0.0,
|
0.0,
|
||||||
|
max_files=save_attempts,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -277,6 +283,11 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi):
|
|||||||
detected_state = self.labelmap[best_id]
|
detected_state = self.labelmap[best_id]
|
||||||
|
|
||||||
if self._should_save_image(camera, detected_state, score):
|
if self._should_save_image(camera, detected_state, score):
|
||||||
|
save_attempts = (
|
||||||
|
self.model_config.save_attempts
|
||||||
|
if self.model_config.save_attempts is not None
|
||||||
|
else 100
|
||||||
|
)
|
||||||
write_classification_attempt(
|
write_classification_attempt(
|
||||||
self.train_dir,
|
self.train_dir,
|
||||||
cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),
|
cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),
|
||||||
@ -284,6 +295,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi):
|
|||||||
now,
|
now,
|
||||||
detected_state,
|
detected_state,
|
||||||
score,
|
score,
|
||||||
|
max_files=save_attempts,
|
||||||
)
|
)
|
||||||
|
|
||||||
if score < self.model_config.threshold:
|
if score < self.model_config.threshold:
|
||||||
@ -482,6 +494,11 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.interpreter is None:
|
if self.interpreter is None:
|
||||||
|
save_attempts = (
|
||||||
|
self.model_config.save_attempts
|
||||||
|
if self.model_config.save_attempts is not None
|
||||||
|
else 200
|
||||||
|
)
|
||||||
write_classification_attempt(
|
write_classification_attempt(
|
||||||
self.train_dir,
|
self.train_dir,
|
||||||
cv2.cvtColor(crop, cv2.COLOR_RGB2BGR),
|
cv2.cvtColor(crop, cv2.COLOR_RGB2BGR),
|
||||||
@ -489,6 +506,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
now,
|
now,
|
||||||
"unknown",
|
"unknown",
|
||||||
0.0,
|
0.0,
|
||||||
|
max_files=save_attempts,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -506,6 +524,11 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
score = round(probs[best_id], 2)
|
score = round(probs[best_id], 2)
|
||||||
self.__update_metrics(datetime.datetime.now().timestamp() - now)
|
self.__update_metrics(datetime.datetime.now().timestamp() - now)
|
||||||
|
|
||||||
|
save_attempts = (
|
||||||
|
self.model_config.save_attempts
|
||||||
|
if self.model_config.save_attempts is not None
|
||||||
|
else 200
|
||||||
|
)
|
||||||
write_classification_attempt(
|
write_classification_attempt(
|
||||||
self.train_dir,
|
self.train_dir,
|
||||||
cv2.cvtColor(crop, cv2.COLOR_RGB2BGR),
|
cv2.cvtColor(crop, cv2.COLOR_RGB2BGR),
|
||||||
@ -513,7 +536,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
|||||||
now,
|
now,
|
||||||
self.labelmap[best_id],
|
self.labelmap[best_id],
|
||||||
score,
|
score,
|
||||||
max_files=200,
|
max_files=save_attempts,
|
||||||
)
|
)
|
||||||
|
|
||||||
if score < self.model_config.threshold:
|
if score < self.model_config.threshold:
|
||||||
|
|||||||
86
frigate/detectors/plugins/axengine.py
Normal file
86
frigate/detectors/plugins/axengine.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import axengine as axe
|
||||||
|
|
||||||
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
|
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||||
|
from frigate.util.model import post_process_yolo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DETECTOR_KEY = "axengine"
|
||||||
|
|
||||||
|
supported_models = {
|
||||||
|
ModelTypeEnum.yologeneric: "frigate-yolov9-.*$",
|
||||||
|
}
|
||||||
|
|
||||||
|
model_cache_dir = os.path.join(MODEL_CACHE_DIR, "axengine_cache/")
|
||||||
|
|
||||||
|
|
||||||
|
class AxengineDetectorConfig(BaseDetectorConfig):
|
||||||
|
type: Literal[DETECTOR_KEY]
|
||||||
|
|
||||||
|
|
||||||
|
class Axengine(DetectionApi):
|
||||||
|
type_key = DETECTOR_KEY
|
||||||
|
|
||||||
|
def __init__(self, config: AxengineDetectorConfig):
|
||||||
|
logger.info("__init__ axengine")
|
||||||
|
super().__init__(config)
|
||||||
|
self.height = config.model.height
|
||||||
|
self.width = config.model.width
|
||||||
|
model_path = config.model.path or "frigate-yolov9-tiny"
|
||||||
|
model_props = self.parse_model_input(model_path)
|
||||||
|
self.session = axe.InferenceSession(model_props["path"])
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_model_input(self, model_path):
|
||||||
|
model_props = {}
|
||||||
|
model_props["preset"] = True
|
||||||
|
|
||||||
|
model_matched = False
|
||||||
|
|
||||||
|
for model_type, pattern in supported_models.items():
|
||||||
|
if re.match(pattern, model_path):
|
||||||
|
model_matched = True
|
||||||
|
model_props["model_type"] = model_type
|
||||||
|
|
||||||
|
if model_matched:
|
||||||
|
model_props["filename"] = model_path + ".axmodel"
|
||||||
|
model_props["path"] = model_cache_dir + model_props["filename"]
|
||||||
|
|
||||||
|
if not os.path.isfile(model_props["path"]):
|
||||||
|
self.download_model(model_props["filename"])
|
||||||
|
else:
|
||||||
|
supported_models_str = ", ".join(model[1:-1] for model in supported_models)
|
||||||
|
raise Exception(
|
||||||
|
f"Model {model_path} is unsupported. Provide your own model or choose one of the following: {supported_models_str}"
|
||||||
|
)
|
||||||
|
return model_props
|
||||||
|
|
||||||
|
def download_model(self, filename):
|
||||||
|
if not os.path.isdir(model_cache_dir):
|
||||||
|
os.mkdir(model_cache_dir)
|
||||||
|
|
||||||
|
GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com")
|
||||||
|
urllib.request.urlretrieve(
|
||||||
|
f"{GITHUB_ENDPOINT}/ivanshi1108/assets/releases/download/v0.16.2/{filename}",
|
||||||
|
model_cache_dir + filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_raw(self, tensor_input):
|
||||||
|
results = None
|
||||||
|
results = self.session.run(None, {"images": tensor_input})
|
||||||
|
if self.detector_config.model.model_type == ModelTypeEnum.yologeneric:
|
||||||
|
return post_process_yolo(results, self.width, self.height)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f'Model type "{self.detector_config.model.model_type}" is currently not supported.'
|
||||||
|
)
|
||||||
@ -5,7 +5,7 @@ import shutil
|
|||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from peewee import fn
|
from peewee import SQL, fn
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import RECORD_DIR
|
from frigate.const import RECORD_DIR
|
||||||
@ -44,13 +44,19 @@ class StorageMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# calculate MB/hr
|
# calculate MB/hr from last 100 segments
|
||||||
try:
|
try:
|
||||||
bandwidth = round(
|
# Subquery to get last 100 segments, then average their bandwidth
|
||||||
Recordings.select(fn.AVG(bandwidth_equation))
|
last_100 = (
|
||||||
|
Recordings.select(bandwidth_equation.alias("bw"))
|
||||||
.where(Recordings.camera == camera, Recordings.segment_size > 0)
|
.where(Recordings.camera == camera, Recordings.segment_size > 0)
|
||||||
|
.order_by(Recordings.start_time.desc())
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.scalar()
|
.alias("recent")
|
||||||
|
)
|
||||||
|
|
||||||
|
bandwidth = round(
|
||||||
|
Recordings.select(fn.AVG(SQL("bw"))).from_(last_100).scalar()
|
||||||
* 3600,
|
* 3600,
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -330,7 +330,7 @@ def collect_state_classification_examples(
|
|||||||
1. Queries review items from specified cameras
|
1. Queries review items from specified cameras
|
||||||
2. Selects 100 balanced timestamps across the data
|
2. Selects 100 balanced timestamps across the data
|
||||||
3. Extracts keyframes from recordings (cropped to specified regions)
|
3. Extracts keyframes from recordings (cropped to specified regions)
|
||||||
4. Selects 20 most visually distinct images
|
4. Selects 24 most visually distinct images
|
||||||
5. Saves them to the dataset directory
|
5. Saves them to the dataset directory
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -660,7 +660,6 @@ def collect_object_classification_examples(
|
|||||||
Args:
|
Args:
|
||||||
model_name: Name of the classification model
|
model_name: Name of the classification model
|
||||||
label: Object label to collect (e.g., "person", "car")
|
label: Object label to collect (e.g., "person", "car")
|
||||||
cameras: List of camera names to collect examples from
|
|
||||||
"""
|
"""
|
||||||
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
|
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
|
||||||
temp_dir = os.path.join(dataset_dir, "temp")
|
temp_dir = os.path.join(dataset_dir, "temp")
|
||||||
|
|||||||
@ -170,6 +170,10 @@
|
|||||||
"label": "Download snapshot",
|
"label": "Download snapshot",
|
||||||
"aria": "Download snapshot"
|
"aria": "Download snapshot"
|
||||||
},
|
},
|
||||||
|
"downloadCleanSnapshot": {
|
||||||
|
"label": "Download clean snapshot",
|
||||||
|
"aria": "Download clean snapshot"
|
||||||
|
},
|
||||||
"viewTrackingDetails": {
|
"viewTrackingDetails": {
|
||||||
"label": "View tracking details",
|
"label": "View tracking details",
|
||||||
"aria": "Show the tracking details"
|
"aria": "Show the tracking details"
|
||||||
|
|||||||
@ -108,6 +108,18 @@ export default function SearchResultActions({
|
|||||||
</a>
|
</a>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{searchResult.has_snapshot &&
|
||||||
|
config?.cameras[searchResult.camera].snapshots.clean_copy && (
|
||||||
|
<MenuItem aria-label={t("itemMenu.downloadCleanSnapshot.aria")}>
|
||||||
|
<a
|
||||||
|
className="flex items-center"
|
||||||
|
href={`${baseUrl}api/events/${searchResult.id}/snapshot-clean.webp`}
|
||||||
|
download={`${searchResult.camera}_${searchResult.label}-clean.webp`}
|
||||||
|
>
|
||||||
|
<span>{t("itemMenu.downloadCleanSnapshot.label")}</span>
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{searchResult.data.type == "object" && (
|
{searchResult.data.type == "object" && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.viewTrackingDetails.aria")}
|
aria-label={t("itemMenu.viewTrackingDetails.aria")}
|
||||||
|
|||||||
@ -69,6 +69,20 @@ export default function DetailActionsMenu({
|
|||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{search.has_snapshot &&
|
||||||
|
config?.cameras[search.camera].snapshots.clean_copy && (
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<a
|
||||||
|
className="w-full"
|
||||||
|
href={`${baseUrl}api/events/${search.id}/snapshot-clean.webp`}
|
||||||
|
download={`${search.camera}_${search.label}-clean.webp`}
|
||||||
|
>
|
||||||
|
<div className="flex cursor-pointer items-center gap-2">
|
||||||
|
<span>{t("itemMenu.downloadCleanSnapshot.label")}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
{search.has_clip && (
|
{search.has_clip && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -498,7 +498,7 @@ export default function SearchDetailDialog({
|
|||||||
|
|
||||||
const views = [...SEARCH_TABS];
|
const views = [...SEARCH_TABS];
|
||||||
|
|
||||||
if (search.data.type != "object" || !search.has_clip) {
|
if (!search.has_clip) {
|
||||||
const index = views.indexOf("tracking_details");
|
const index = views.indexOf("tracking_details");
|
||||||
views.splice(index, 1);
|
views.splice(index, 1);
|
||||||
}
|
}
|
||||||
@ -548,7 +548,7 @@ export default function SearchDetailDialog({
|
|||||||
"relative flex items-center justify-between",
|
"relative flex items-center justify-between",
|
||||||
"w-full",
|
"w-full",
|
||||||
// match dialog's max-width classes
|
// match dialog's max-width classes
|
||||||
"sm:max-w-xl md:max-w-4xl lg:max-w-[70%]",
|
"max-h-[95dvh] max-w-[85%] xl:max-w-[70%]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -594,8 +594,7 @@ export default function SearchDetailDialog({
|
|||||||
ref={isDesktop ? dialogContentRef : undefined}
|
ref={isDesktop ? dialogContentRef : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"scrollbar-container overflow-y-auto",
|
"scrollbar-container overflow-y-auto",
|
||||||
isDesktop &&
|
isDesktop && "max-h-[95dvh] max-w-[85%] xl:max-w-[70%]",
|
||||||
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-[70%]",
|
|
||||||
isMobile && "flex h-full flex-col px-4",
|
isMobile && "flex h-full flex-col px-4",
|
||||||
)}
|
)}
|
||||||
onEscapeKeyDown={(event) => {
|
onEscapeKeyDown={(event) => {
|
||||||
|
|||||||
@ -622,7 +622,7 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
isDesktop && "justify-between overflow-hidden md:basis-2/5",
|
isDesktop && "justify-between overflow-hidden lg:basis-2/5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isDesktop && tabs && (
|
{isDesktop && tabs && (
|
||||||
@ -900,96 +900,99 @@ function LifecycleIconRow({
|
|||||||
<div className="text-md flex items-start break-words text-left">
|
<div className="text-md flex items-start break-words text-left">
|
||||||
{getLifecycleItemDescription(item)}
|
{getLifecycleItemDescription(item)}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground">
|
{/* Only show Score/Ratio/Area for object events, not for audio (heard) or manual API (external) events */}
|
||||||
<div className="flex items-center gap-1.5">
|
{item.class_type !== "heard" && item.class_type !== "external" && (
|
||||||
<span className="text-primary-variant">
|
<div className="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground">
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.score")}
|
<div className="flex items-center gap-1.5">
|
||||||
</span>
|
<span className="text-primary-variant">
|
||||||
<span className="font-medium text-primary">{score}</span>
|
{t("trackingDetails.lifecycleItemDesc.header.score")}
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="text-primary-variant">
|
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-primary">{ratio}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="text-primary-variant">
|
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.area")}{" "}
|
|
||||||
{attributeAreaPx !== undefined &&
|
|
||||||
attributeAreaPct !== undefined && (
|
|
||||||
<span className="text-primary-variant">
|
|
||||||
({getTranslatedLabel(item.data.label)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{areaPx !== undefined && areaPct !== undefined ? (
|
|
||||||
<span className="font-medium text-primary">
|
|
||||||
{t("information.pixels", { ns: "common", area: areaPx })} ·{" "}
|
|
||||||
{areaPct}%
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
<span className="font-medium text-primary">{score}</span>
|
||||||
<span>N/A</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{attributeAreaPx !== undefined &&
|
|
||||||
attributeAreaPct !== undefined && (
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="text-primary-variant">
|
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.area")} (
|
|
||||||
{getTranslatedLabel(item.data.attribute)})
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-primary">
|
|
||||||
{t("information.pixels", {
|
|
||||||
ns: "common",
|
|
||||||
area: attributeAreaPx,
|
|
||||||
})}{" "}
|
|
||||||
· {attributeAreaPct}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.data?.zones && item.data.zones.length > 0 && (
|
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
||||||
{item.data.zones.map((zone, zidx) => {
|
|
||||||
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={`${zone}-${zidx}`}
|
|
||||||
variant="outline"
|
|
||||||
className="inline-flex cursor-pointer items-center gap-2"
|
|
||||||
onClick={(e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSelectedZone(zone);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
borderColor: `rgba(${color}, 0.6)`,
|
|
||||||
background: `rgba(${color}, 0.08)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="size-1 rounded-full"
|
|
||||||
style={{
|
|
||||||
display: "inline-block",
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
backgroundColor: `rgb(${color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
item.data?.zones_friendly_names?.[zidx] === zone &&
|
|
||||||
"smart-capitalize",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.data?.zones_friendly_names?.[zidx]}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-1.5">
|
||||||
</div>
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">{ratio}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.area")}{" "}
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
({getTranslatedLabel(item.data.label)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{areaPx !== undefined && areaPct !== undefined ? (
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{t("information.pixels", { ns: "common", area: areaPx })}{" "}
|
||||||
|
· {areaPct}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>N/A</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.area")} (
|
||||||
|
{getTranslatedLabel(item.data.attribute)})
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{t("information.pixels", {
|
||||||
|
ns: "common",
|
||||||
|
area: attributeAreaPx,
|
||||||
|
})}{" "}
|
||||||
|
· {attributeAreaPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.data?.zones && item.data.zones.length > 0 && (
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
|
{item.data.zones.map((zone, zidx) => {
|
||||||
|
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={`${zone}-${zidx}`}
|
||||||
|
variant="outline"
|
||||||
|
className="inline-flex cursor-pointer items-center gap-2"
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedZone(zone);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
borderColor: `rgba(${color}, 0.6)`,
|
||||||
|
background: `rgba(${color}, 0.08)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="size-1 rounded-full"
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: `rgb(${color})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
item.data?.zones_friendly_names?.[zidx] === zone &&
|
||||||
|
"smart-capitalize",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.data?.zones_friendly_names?.[zidx]}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
|
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
|
||||||
|
|||||||
@ -305,6 +305,7 @@ export type CustomClassificationModelConfig = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
|
save_attempts?: number;
|
||||||
object_config?: {
|
object_config?: {
|
||||||
objects: string[];
|
objects: string[];
|
||||||
classification_type: string;
|
classification_type: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user