Skip to content

Commit ce9c781

Browse files
authored
Merge pull request #1033 from john-/yolo_component_squashed
yolo component
2 parents 0562c67 + ebaf845 commit ce9c781

9 files changed

Lines changed: 766 additions & 1 deletion

File tree

.mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ ignore_missing_imports = True
7777
ignore_missing_imports = True
7878

7979
[mypy-telegram-ext.*]
80+
ignore_missing_imports = True
81+
82+
[mypy-ultralytics.*]
8083
ignore_missing_imports = True
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Component } from "@site/src/types";
2+
3+
const ComponentMetadata: Component = {
4+
title: "Ultralytics YOLO",
5+
name: "yolo",
6+
description: "Ultralytics YOLO supports a wide range of YOLO models, from early versions like YOLOv3 to the latest YOLO11",
7+
image: "https://cdn.prod.website-files.com/680a070c3b99253410dd3dcf/680a070c3b99253410dd3e88_UltralyticsYOLO_full_blue.svg",
8+
tags: ['object_detector'],
9+
};
10+
11+
export default ComponentMetadata;
12+

docs/src/pages/components-explorer/components/yolo/config.json

Lines changed: 394 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import ComponentConfiguration from "@site/src/pages/components-explorer/_components/ComponentConfiguration";
2+
import ComponentHeader from "@site/src/pages/components-explorer/_components/ComponentHeader";
3+
import ComponentTroubleshooting from "@site/src/pages/components-explorer/_components/ComponentTroubleshooting/index.mdx";
4+
5+
import ComponentMetadata from "./_meta";
6+
import config from "./config.json";
7+
8+
<ComponentHeader meta={ComponentMetadata} />
9+
10+
Ultralytics YOLO supports a wide range of models, from early versions like YOLOv3 to the latest YOLO11.
11+
12+
:::warning
13+
14+
This component has undergone limited testing. In addition to partial functional testing, only the following models have been confirmed to work: yolov5mu.pt, yolov8n, and yolo11s.pt
15+
16+
:::
17+
18+
:::note
19+
20+
`yolo` component uses the official [`ultralytics`](https://docs.ultralytics.com/usage/python) python package. A GPU is used when available.
21+
22+
:::
23+
24+
:::info
25+
26+
Models are not installed by default. See below for steps to define the model as well as make them available to Viseron.
27+
28+
:::
29+
30+
## Configuration
31+
32+
<details>
33+
<summary>Configuration example</summary>
34+
35+
```yaml title="/config/config.yaml"
36+
yolo:
37+
object_detector:
38+
model_path: /detectors/models/yolo/my_model.pt
39+
cameras:
40+
viseron_camera1:
41+
fps: 1
42+
scan_on_motion_only: true
43+
log_all_objects: false
44+
labels:
45+
- label: dog
46+
confidence: 0.7
47+
trigger_event_recording: false
48+
- label: cat
49+
confidence: 0.8
50+
```
51+
52+
</details>
53+
54+
<ComponentConfiguration meta={ComponentMetadata} config={config} />
55+
56+
### Pre-trained models
57+
58+
These steps should assist in locating models, configuring your container to access them, and configuring Viseron to use them.
59+
60+
#### Finding models
61+
62+
Pre-trained YOLO models can be found online or you can train them yourself.
63+
64+
Examples of where to find pre-trained models:
65+
66+
- [Ultralytics](https://docs.ultralytics.com/models/)
67+
- [Roboflow](https://universe.roboflow.com/)
68+
- [Hugging Face](https://huggingface.co/models?pipeline_tag=object-detection&sort=trending)
69+
70+
There are models for many different tasks, including object detection. If you are not sure if there is a problem with Viseron please confirm your
71+
Viseron environment with a stock YOLO model from Ultralytics. For example: [yolov8n.pt](https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov8n.pt)
72+
73+
This component does not provide any training capabilities. See the [Ultralytics training](https://docs.ultralytics.com/modes/train/) documentation for more information.
74+
75+
#### Where to place models
76+
77+
Place your YOLO models in a directory of your choice.
78+
79+
There will be a later step to map the directory to the container. Therefore, choose a location supported by docker compose. If in doubt, do not use a SMB or NFS share.
80+
81+
#### Configuring Docker to make models available to Viseron
82+
83+
The following `docker-compose.yaml` snippet will show how to map the directory above to the container:
84+
```yaml title="/docker-compose.yaml"
85+
volumes:
86+
- {models path}:/detectors/models/yolo
87+
```
88+
89+
This is the only change to `docker-compose.yaml` required for this component.
90+
91+
#### Configuring Viseron to use a model
92+
93+
Modify the `model_path` setting in your Viseron `config.yaml` to point to one of the model(s) you installed. See the example above.
94+
95+
Only one model can be used at a time.
96+
97+
### Image resizing
98+
99+
Images inferenced by the component are resized by the underlying `ultralytics` package to match the model's input size.
100+
101+
There is no functionality to resize the image in the `yolo` component configuration before inferencing.
102+
103+
### Labels
104+
105+
When Viseron loads the model, it will print that model's labels to the log.
106+
```
107+
cd {location of Viseron docker-compose.yaml}
108+
docker compose logs | grep "Labels"
109+
viseron | 2025-05-29 08:19:04.943 [INFO ] [viseron.components.yolo.object_detector] - Labels: {0: 'bicycle', 1: 'bird', 2: 'bus', 3: 'car', 4: 'cat', 5: 'dog', 6: 'motorcycle', 7: 'person', 8: 'truck', 9: 'squirrel', 10: 'car-light', 11: 'rabbit', 12: 'fox', 13: 'opossum', 14: 'skunk', 15: 'racoon'}
110+
```
111+
112+
<ComponentTroubleshooting meta={ComponentMetadata} />
113+

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ sqlalchemy==2.0.30
3434
watchdog==4.0.0
3535
python-telegram-bot==21.4
3636
onvif-zeep==0.2.12
37+
ultralytics==8.3.146

viseron/components/webserver/websocket_api/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async def forward_event(event: Event) -> None:
161161

162162
@debounce(
163163
wait=message["debounce"],
164-
options=DebounceOptions( # pylint: disable=unexpected-keyword-arg
164+
options=DebounceOptions(
165165
time_window=message["debounce"],
166166
),
167167
)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""YOLO component."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
import voluptuous as vol
8+
9+
from viseron import Viseron
10+
from viseron.domains import RequireDomain, setup_domain
11+
from viseron.domains.object_detector import (
12+
BASE_CONFIG_SCHEMA as OBJECT_DETECTOR_BASE_CONFIG_SCHEMA,
13+
)
14+
from viseron.domains.object_detector.const import CONFIG_CAMERAS
15+
from viseron.helpers.schemas import FLOAT_MIN_ZERO_MAX_ONE
16+
from viseron.helpers.validators import Maybe
17+
18+
from .const import (
19+
COMPONENT,
20+
CONFIG_DEVICE,
21+
CONFIG_HALF_PRECISION,
22+
CONFIG_IOU,
23+
CONFIG_MIN_CONFIDENCE,
24+
CONFIG_MODEL_PATH,
25+
CONFIG_OBJECT_DETECTOR,
26+
DEFAULT_DEVICE,
27+
DEFAULT_HALF_PRECISION,
28+
DEFAULT_IOU,
29+
DEFAULT_MIN_CONFIDENCE,
30+
DEFAULT_MODEL_PATH,
31+
DESC_COMPONENT,
32+
DESC_DEVICE,
33+
DESC_HALF_PRECISION,
34+
DESC_IOU,
35+
DESC_MIN_CONFIDENCE,
36+
DESC_MODEL_PATH,
37+
DESC_OBJECT_DETECTOR,
38+
)
39+
40+
OBJECT_DETECTOR_SCHEMA = OBJECT_DETECTOR_BASE_CONFIG_SCHEMA.extend(
41+
{
42+
vol.Optional(
43+
CONFIG_MODEL_PATH,
44+
default=DEFAULT_MODEL_PATH,
45+
description=DESC_MODEL_PATH,
46+
): str,
47+
vol.Optional(
48+
CONFIG_MIN_CONFIDENCE,
49+
default=DEFAULT_MIN_CONFIDENCE,
50+
description=DESC_MIN_CONFIDENCE,
51+
): FLOAT_MIN_ZERO_MAX_ONE,
52+
vol.Optional(
53+
CONFIG_IOU,
54+
default=DEFAULT_IOU,
55+
description=DESC_IOU,
56+
): FLOAT_MIN_ZERO_MAX_ONE,
57+
vol.Optional(
58+
CONFIG_HALF_PRECISION,
59+
default=DEFAULT_HALF_PRECISION,
60+
description=DESC_HALF_PRECISION,
61+
): bool,
62+
vol.Optional(
63+
CONFIG_DEVICE,
64+
default=DEFAULT_DEVICE,
65+
description=DESC_DEVICE,
66+
): Maybe(str),
67+
}
68+
)
69+
70+
CONFIG_SCHEMA = vol.Schema(
71+
{
72+
vol.Required(COMPONENT, description=DESC_COMPONENT): vol.Schema(
73+
{
74+
vol.Required(
75+
CONFIG_OBJECT_DETECTOR, description=DESC_OBJECT_DETECTOR
76+
): OBJECT_DETECTOR_SCHEMA,
77+
}
78+
)
79+
},
80+
extra=vol.ALLOW_EXTRA,
81+
)
82+
83+
84+
def setup(vis: Viseron, config: dict[str, Any]) -> bool:
85+
"""Set up YOLO component."""
86+
config = config[COMPONENT]
87+
88+
if config.get(CONFIG_OBJECT_DETECTOR, None):
89+
for camera_identifier in config[CONFIG_OBJECT_DETECTOR][CONFIG_CAMERAS].keys():
90+
setup_domain(
91+
vis,
92+
COMPONENT,
93+
CONFIG_OBJECT_DETECTOR,
94+
config,
95+
identifier=camera_identifier,
96+
require_domains=[
97+
RequireDomain(
98+
domain="camera",
99+
identifier=camera_identifier,
100+
)
101+
],
102+
)
103+
104+
return True

viseron/components/yolo/const.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Constants for the YOLO component."""
2+
from typing import Final
3+
4+
COMPONENT = "yolo"
5+
6+
# CONFIG_SCHEMA constants
7+
CONFIG_OBJECT_DETECTOR = "object_detector"
8+
9+
# OBJECT_DETECTOR_SCHEMA constants
10+
CONFIG_MODEL_PATH = "model_path"
11+
CONFIG_MIN_CONFIDENCE = "min_confidence"
12+
CONFIG_IOU = "iou"
13+
CONFIG_HALF_PRECISION = "half_precision"
14+
CONFIG_DEVICE = "device"
15+
16+
DEFAULT_MODEL_PATH = "/detectors/models/yolo/default.pt"
17+
DEFAULT_MIN_CONFIDENCE = 0.25
18+
DEFAULT_IOU = 0.7
19+
DEFAULT_HALF_PRECISION = False
20+
DEFAULT_DEVICE: Final = None
21+
22+
DESC_COMPONENT = "YOLO configuration."
23+
DESC_OBJECT_DETECTOR = "Object detector domain config."
24+
25+
DESC_MODEL_PATH = (
26+
"Path to a YOLO model."
27+
"More information "
28+
"<a href=https://docs.ultralytics.com/models>here</a>."
29+
)
30+
DESC_MIN_CONFIDENCE = (
31+
"Minimum confidence to consider a detection.<br>"
32+
"This minimum is enforced during inference before being filtered by values "
33+
"in <code>labels</code>"
34+
)
35+
DESC_IOU = "Intersection Over Union (IoU) threshold for Non-Maximum Suppression (NMS)."
36+
DESC_HALF_PRECISION = (
37+
"Enable/disable half precision accuracy.<br>"
38+
"If your GPU supports FP16, enabling this might give you a performance increase."
39+
)
40+
DESC_DEVICE = "Specifies the device for inference (e.g., cpu, cuda:0 or 0)."
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""YOLO object detector."""
2+
3+
import logging
4+
from pathlib import Path
5+
6+
import numpy as np
7+
from ultralytics import YOLO
8+
9+
from viseron import Viseron
10+
from viseron.domains.object_detector import AbstractObjectDetector
11+
from viseron.domains.object_detector.detected_object import DetectedObject
12+
from viseron.exceptions import DomainNotReady
13+
14+
from .const import (
15+
COMPONENT,
16+
CONFIG_DEVICE,
17+
CONFIG_HALF_PRECISION,
18+
CONFIG_IOU,
19+
CONFIG_MIN_CONFIDENCE,
20+
CONFIG_MODEL_PATH,
21+
CONFIG_OBJECT_DETECTOR,
22+
)
23+
24+
LOGGER = logging.getLogger(__name__)
25+
26+
27+
def setup(vis: Viseron, config, identifier) -> bool:
28+
"""Set up the YOLO object_detector domain."""
29+
ObjectDetector(vis, config, identifier)
30+
31+
return True
32+
33+
34+
class ObjectDetector(AbstractObjectDetector):
35+
"""YOLO object detection."""
36+
37+
def __init__(self, vis: Viseron, config, camera_identifier) -> None:
38+
super().__init__(
39+
vis, COMPONENT, config[CONFIG_OBJECT_DETECTOR], camera_identifier
40+
)
41+
42+
try:
43+
model = Path(self._config[CONFIG_MODEL_PATH])
44+
self._detector = YOLO(model)
45+
except Exception as error:
46+
LOGGER.error("YOLO model file not loaded: %s", error)
47+
raise DomainNotReady from error
48+
49+
LOGGER.info(f"Loaded YOLO model: {model}")
50+
LOGGER.info(f"Labels: {self._detector.names}")
51+
52+
def preprocess(self, frame):
53+
"""Preprocess frame before detection."""
54+
55+
return np.array(frame)
56+
57+
def postprocess(self, results):
58+
"""Return yolo detections as DetectedObject."""
59+
60+
objects = []
61+
62+
for result in results:
63+
classes_names = result.names
64+
65+
for box in result.boxes:
66+
cls = int(box.cls[0])
67+
[x1, y1, x2, y2] = box.xyxy[0]
68+
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
69+
objects.append(
70+
DetectedObject.from_absolute(
71+
label=classes_names[cls],
72+
confidence=float(box.conf),
73+
x1=x1,
74+
y1=y1,
75+
x2=x2,
76+
y2=y2,
77+
frame_res=self._camera.resolution,
78+
model_res=result.orig_shape[::-1],
79+
)
80+
)
81+
return objects
82+
83+
def return_objects(self, frame):
84+
"""Perform object detection."""
85+
try:
86+
results = self._detector.predict(
87+
frame,
88+
conf=self._config[CONFIG_MIN_CONFIDENCE],
89+
iou=self._config[CONFIG_IOU],
90+
half=self._config[CONFIG_HALF_PRECISION],
91+
device=self._config[CONFIG_DEVICE],
92+
verbose=False,
93+
)
94+
except ValueError as error:
95+
LOGGER.error(f"Error calling yolo prediction check yolo config: {error}")
96+
return []
97+
98+
return self.postprocess(results)

0 commit comments

Comments
 (0)