@camera.ui/camera-ui-coreml 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -0
- package/LICENSE.md +22 -0
- package/README.md +1 -0
- package/config.schema.json +43 -0
- package/package.json +45 -0
- package/requirements.txt +4 -0
- package/src/camera_detector.py +158 -0
- package/src/detector_defaults.py +30 -0
- package/src/main.py +128 -0
- package/src/object_detector.py +190 -0
- package/src/plugin_typings.py +5 -0
- package/src/utils.py +44 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
All notable changes to this project will be documented in this file.
|
|
2
|
+
|
|
3
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
4
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [X.X.X] - ???
|
|
7
|
+
|
|
8
|
+
- Initial Release
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Contributing
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2024 seydx <dev@seydx.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @camera.ui/camera-ui-coreml
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": {
|
|
3
|
+
"model": {
|
|
4
|
+
"type": "string",
|
|
5
|
+
"title": "Model",
|
|
6
|
+
"description": "Model to use for object detection",
|
|
7
|
+
"required": true,
|
|
8
|
+
"store": true,
|
|
9
|
+
"defaultValue": "yolov9m_320",
|
|
10
|
+
"enum": [
|
|
11
|
+
"yolo3-tinyu",
|
|
12
|
+
"yolo3-tinyu_320",
|
|
13
|
+
"yolov5nu",
|
|
14
|
+
"yolov5nu_320",
|
|
15
|
+
"yolov5mu",
|
|
16
|
+
"yolov5mu_320",
|
|
17
|
+
"yolov8n",
|
|
18
|
+
"yolov8n_320",
|
|
19
|
+
"yolov8s",
|
|
20
|
+
"yolov8s_320",
|
|
21
|
+
"yolov8m",
|
|
22
|
+
"yolov8m_320",
|
|
23
|
+
"yolov9t",
|
|
24
|
+
"yolov9t_320",
|
|
25
|
+
"yolov9s",
|
|
26
|
+
"yolov9s_320",
|
|
27
|
+
"yolov9m",
|
|
28
|
+
"yolov9m_320",
|
|
29
|
+
"yolov9c",
|
|
30
|
+
"yolov9c_320"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"precision": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"title": "Precision",
|
|
36
|
+
"description": "Precision of the model",
|
|
37
|
+
"required": true,
|
|
38
|
+
"store": true,
|
|
39
|
+
"defaultValue": "FP16",
|
|
40
|
+
"enum": ["INT8", "FP16"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"displayName": "CoreML",
|
|
3
|
+
"name": "@camera.ui/camera-ui-coreml",
|
|
4
|
+
"version": "0.0.7",
|
|
5
|
+
"description": "camera.ui coreml plugin",
|
|
6
|
+
"author": "seydx (https://github.com/seydx/camera.ui)",
|
|
7
|
+
"main": "./src/main.py",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"format": "ruff format",
|
|
11
|
+
"install-updates": "npm i --save",
|
|
12
|
+
"lint": "ruff check --fix",
|
|
13
|
+
"update": "updates --update ./",
|
|
14
|
+
"prepublishOnly": "npm i --package-lock-only && npm run format && npm run lint"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"updates": "^16.4.0"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/seydx/camera.ui/issues"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"camera.ui": ">=0.0.1",
|
|
25
|
+
"node": ">=18.12.0"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/seydx/camera.ui#readme",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"camera-ui-plugin",
|
|
30
|
+
"motion",
|
|
31
|
+
"object",
|
|
32
|
+
"coreml",
|
|
33
|
+
"detection"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/seydx/camera.ui.git"
|
|
39
|
+
},
|
|
40
|
+
"camera.ui": {
|
|
41
|
+
"pythonVersion": "3.11",
|
|
42
|
+
"extension": "objectDetection",
|
|
43
|
+
"dependencies": []
|
|
44
|
+
}
|
|
45
|
+
}
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Optional, cast
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from camera_ui_python_types import (
|
|
6
|
+
CameraDevice,
|
|
7
|
+
CameraStorage,
|
|
8
|
+
Detection,
|
|
9
|
+
MotionState,
|
|
10
|
+
ObjectClass,
|
|
11
|
+
PluginAPI,
|
|
12
|
+
PluginLogger,
|
|
13
|
+
)
|
|
14
|
+
from object_detector import ObjectDetector
|
|
15
|
+
from plugin_typings import CameraStorageValues
|
|
16
|
+
|
|
17
|
+
background_tasks = set[asyncio.Task[Any]]()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CameraDetector:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
camera: CameraDevice,
|
|
24
|
+
object_detector: ObjectDetector,
|
|
25
|
+
api: PluginAPI,
|
|
26
|
+
logger: PluginLogger,
|
|
27
|
+
has_motion: list[str],
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
|
|
31
|
+
self.camera = camera
|
|
32
|
+
self.api = api
|
|
33
|
+
self.logger = logger
|
|
34
|
+
self.object_detector = object_detector
|
|
35
|
+
|
|
36
|
+
self.has_motion = has_motion
|
|
37
|
+
self.started = False
|
|
38
|
+
self.closed = False
|
|
39
|
+
self.frame_generation_task: Optional[asyncio.Task[Any]] = None
|
|
40
|
+
|
|
41
|
+
self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
|
|
42
|
+
self.camera.on_connected.subscribe(lambda connected: self.start() if connected else self.close())
|
|
43
|
+
self.camera.on_motion_detected.subscribe(self.__on_motion_detected)
|
|
44
|
+
|
|
45
|
+
def start(self):
|
|
46
|
+
if not self.started:
|
|
47
|
+
self.started = True
|
|
48
|
+
self.closed = False
|
|
49
|
+
|
|
50
|
+
self.logger.log(f"Starting object detection for camera {self.camera.name}...")
|
|
51
|
+
|
|
52
|
+
self.frame_generation_task = asyncio.create_task(
|
|
53
|
+
self.__detect(), name=f"ObjectDetection-{self.camera.name}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
background_tasks.add(self.frame_generation_task)
|
|
57
|
+
self.frame_generation_task.add_done_callback(
|
|
58
|
+
lambda _: background_tasks.remove(self.frame_generation_task)
|
|
59
|
+
if self.frame_generation_task in background_tasks
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def close(self):
|
|
64
|
+
if self.started and not self.closed:
|
|
65
|
+
self.started = False
|
|
66
|
+
self.closed = True
|
|
67
|
+
|
|
68
|
+
self.logger.log(f"Stopping object detection for camera {self.camera.name}...")
|
|
69
|
+
|
|
70
|
+
if self.frame_generation_task is not None:
|
|
71
|
+
self.frame_generation_task.cancel()
|
|
72
|
+
self.frame_generation_task = None
|
|
73
|
+
|
|
74
|
+
if self.camera.name in self.has_motion:
|
|
75
|
+
self.has_motion.remove(self.camera.name)
|
|
76
|
+
|
|
77
|
+
def restart(self):
|
|
78
|
+
self.logger.log(f"Restarting object detection for camera {self.camera.name}...")
|
|
79
|
+
|
|
80
|
+
self.close()
|
|
81
|
+
|
|
82
|
+
if self.camera.connected:
|
|
83
|
+
self.start()
|
|
84
|
+
|
|
85
|
+
async def __detect(self):
|
|
86
|
+
try:
|
|
87
|
+
async for frame in self.camera.get_motion_frames():
|
|
88
|
+
frame_image = await frame.to_image(
|
|
89
|
+
{
|
|
90
|
+
"format": {"to": "rgb"},
|
|
91
|
+
"resize": {
|
|
92
|
+
"width": self.object_detector.input_width,
|
|
93
|
+
"height": self.object_detector.input_height,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
detections: list[Detection] = []
|
|
99
|
+
|
|
100
|
+
class_ids, scores, boxes = await self.object_detector.detect(frame_image["image"])
|
|
101
|
+
|
|
102
|
+
for class_id, score, box in zip(class_ids, scores, boxes):
|
|
103
|
+
x1, y1, x2, y2 = box
|
|
104
|
+
|
|
105
|
+
detection: Detection = {
|
|
106
|
+
"id": str(uuid4()),
|
|
107
|
+
"label": cast(ObjectClass, self.object_detector.labels[class_id]),
|
|
108
|
+
"confidence": score,
|
|
109
|
+
"boundingBox": (float(x1), float(y1), float(x2), float(y2)),
|
|
110
|
+
"inputWidth": self.object_detector.input_width,
|
|
111
|
+
"inputHeight": self.object_detector.input_height,
|
|
112
|
+
"origWidth": frame.metadata["origWidth"],
|
|
113
|
+
"origHeight": frame.metadata["origHeight"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
detections.append(detection)
|
|
117
|
+
|
|
118
|
+
await self.camera.update_state("object", {"detections": detections})
|
|
119
|
+
except asyncio.CancelledError:
|
|
120
|
+
pass
|
|
121
|
+
except Exception as error:
|
|
122
|
+
self.logger.error(self.camera.name, "Error generating motion frames", error)
|
|
123
|
+
finally:
|
|
124
|
+
if not self.closed:
|
|
125
|
+
self.restart()
|
|
126
|
+
|
|
127
|
+
def __on_motion_detected(self, state: MotionState) -> None:
|
|
128
|
+
motion_detected = bool(state.get("state", False) or len(state["detections"]) > 0)
|
|
129
|
+
|
|
130
|
+
if motion_detected:
|
|
131
|
+
if self.camera.name not in self.has_motion:
|
|
132
|
+
self.has_motion.append(self.camera.name)
|
|
133
|
+
elif self.camera.name in self.has_motion:
|
|
134
|
+
self.has_motion.remove(self.camera.name)
|
|
135
|
+
|
|
136
|
+
def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
|
|
137
|
+
camera_storage: Optional[CameraStorage[CameraStorageValues]] = (
|
|
138
|
+
self.api.storage_controller.get_camera_storage(self.camera.id)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if camera_storage is not None:
|
|
142
|
+
return camera_storage
|
|
143
|
+
|
|
144
|
+
return self.api.storage_controller.create_camera_storage(
|
|
145
|
+
self,
|
|
146
|
+
self.camera.id,
|
|
147
|
+
{
|
|
148
|
+
"active_object_detections": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"key": "active_object_detections",
|
|
151
|
+
"title": "Active Object Detections",
|
|
152
|
+
"description": "Active object detections",
|
|
153
|
+
"readonly": True,
|
|
154
|
+
"enum": [],
|
|
155
|
+
"onGet": lambda: self.has_motion,
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
available_models = [
|
|
2
|
+
"yolo3-tinyu",
|
|
3
|
+
"yolo3-tinyu_320",
|
|
4
|
+
"yolov5nu",
|
|
5
|
+
"yolov5nu_320",
|
|
6
|
+
"yolov5mu",
|
|
7
|
+
"yolov5mu_320",
|
|
8
|
+
"yolov8n",
|
|
9
|
+
"yolov8n_320",
|
|
10
|
+
"yolov8s",
|
|
11
|
+
"yolov8s_320",
|
|
12
|
+
"yolov8m",
|
|
13
|
+
"yolov8m_320",
|
|
14
|
+
"yolov9t",
|
|
15
|
+
"yolov9t_320",
|
|
16
|
+
"yolov9s",
|
|
17
|
+
"yolov9s_320",
|
|
18
|
+
"yolov9m",
|
|
19
|
+
"yolov9m_320",
|
|
20
|
+
"yolov9c",
|
|
21
|
+
"yolov9c_320",
|
|
22
|
+
# "yolov10n",
|
|
23
|
+
# "yolov10n_320",
|
|
24
|
+
# "yolov10s",
|
|
25
|
+
# "yolov10s_320",
|
|
26
|
+
# "yolov10m",
|
|
27
|
+
# "yolov10m_320",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
available_precision = ["INT8", "FP16"]
|
package/src/main.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
|
|
3
|
+
from camera_detector import CameraDetector
|
|
4
|
+
from camera_ui_python_types import (
|
|
5
|
+
CameraDevice,
|
|
6
|
+
CameraExtension,
|
|
7
|
+
FormSubmitResponse,
|
|
8
|
+
ImageMetadata,
|
|
9
|
+
ObjectDetectionPlugin,
|
|
10
|
+
ObjectDetectionPluginResponse,
|
|
11
|
+
PluginAPI,
|
|
12
|
+
PluginLogger,
|
|
13
|
+
RootSchema,
|
|
14
|
+
)
|
|
15
|
+
from detector_defaults import available_models, available_precision
|
|
16
|
+
from object_detector import ObjectDetector
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CoreML(ObjectDetectionPlugin):
|
|
20
|
+
def __init__(self, logger: PluginLogger, api: PluginAPI):
|
|
21
|
+
self.api = api
|
|
22
|
+
self.logger = logger
|
|
23
|
+
self.camera_devices: dict[str, CameraDevice] = {}
|
|
24
|
+
self.detectors: dict[str, CameraDetector] = {}
|
|
25
|
+
self.object_detector = ObjectDetector(self.api, self.logger)
|
|
26
|
+
self.single_image_detectors: dict[str, ObjectDetector] = {}
|
|
27
|
+
|
|
28
|
+
self.has_motion: list[str] = []
|
|
29
|
+
|
|
30
|
+
self.api.on("finishLaunching", self.start)
|
|
31
|
+
self.api.on("shutdown", self.stop)
|
|
32
|
+
|
|
33
|
+
self.api.device_manager.on("cameraSelected", self.camera_selected)
|
|
34
|
+
self.api.device_manager.on("cameraDeselected", self.camera_deselected)
|
|
35
|
+
|
|
36
|
+
async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
|
|
37
|
+
self.logger.log(f"New camera added ({extension}): {camera.name}")
|
|
38
|
+
self.camera_devices[camera.id] = camera
|
|
39
|
+
self.create_detector(camera)
|
|
40
|
+
|
|
41
|
+
def camera_deselected(self, id: str, extension: CameraExtension) -> None:
|
|
42
|
+
camera = self.camera_devices.get(id)
|
|
43
|
+
|
|
44
|
+
if camera:
|
|
45
|
+
self.logger.log(f"Camera removed ({extension}): {camera.name}")
|
|
46
|
+
self.remove_detector(self.camera_devices[id])
|
|
47
|
+
del self.camera_devices[id]
|
|
48
|
+
|
|
49
|
+
def start(self) -> None:
|
|
50
|
+
self.logger.log("Plugin Started")
|
|
51
|
+
|
|
52
|
+
self.object_detector.initialize()
|
|
53
|
+
|
|
54
|
+
for camera_device in self.camera_devices.values():
|
|
55
|
+
self.create_detector(camera_device)
|
|
56
|
+
|
|
57
|
+
def stop(self) -> None:
|
|
58
|
+
self.logger.log("Plugin Stopped")
|
|
59
|
+
|
|
60
|
+
detectors_copy = list(self.detectors.values())
|
|
61
|
+
|
|
62
|
+
for detector in detectors_copy:
|
|
63
|
+
self.remove_detector(detector.camera)
|
|
64
|
+
|
|
65
|
+
async def onFormSubmit(self, action_id: str, payload: Any) -> Union[FormSubmitResponse, None]:
|
|
66
|
+
print("Form submitted", action_id, payload)
|
|
67
|
+
|
|
68
|
+
def configure_cameras(self, cameras: list[CameraDevice]) -> None:
|
|
69
|
+
for camera in cameras:
|
|
70
|
+
self.camera_devices[camera.id] = camera
|
|
71
|
+
|
|
72
|
+
def create_detector(self, camera: CameraDevice) -> None:
|
|
73
|
+
detector = CameraDetector(camera, self.object_detector, self.api, self.logger, self.has_motion)
|
|
74
|
+
self.detectors[camera.id] = detector
|
|
75
|
+
|
|
76
|
+
def remove_detector(self, camera: CameraDevice) -> None:
|
|
77
|
+
detector = self.detectors[camera.id]
|
|
78
|
+
|
|
79
|
+
if detector:
|
|
80
|
+
detector.close()
|
|
81
|
+
del self.detectors[camera.id]
|
|
82
|
+
|
|
83
|
+
def interfaceSchema(self) -> RootSchema | None:
|
|
84
|
+
return {
|
|
85
|
+
"schema": {
|
|
86
|
+
"models": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"title": "Model",
|
|
89
|
+
"description": "Model to use for object detection",
|
|
90
|
+
"required": True,
|
|
91
|
+
"defaultValue": "yolov9m_320",
|
|
92
|
+
"enum": available_models,
|
|
93
|
+
},
|
|
94
|
+
"precision": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"title": "Precision",
|
|
97
|
+
"description": "Precision of the model",
|
|
98
|
+
"required": True,
|
|
99
|
+
"defaultValue": "FP16",
|
|
100
|
+
"enum": available_precision,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async def detectObjects(
|
|
106
|
+
self, image_path: str, metadata: ImageMetadata, config: dict[str, Any]
|
|
107
|
+
) -> ObjectDetectionPluginResponse:
|
|
108
|
+
model = config.get("models")
|
|
109
|
+
precision = config.get("precision")
|
|
110
|
+
|
|
111
|
+
identifier = f"{model}_{precision}"
|
|
112
|
+
single_image_detector = self.single_image_detectors.get(identifier)
|
|
113
|
+
|
|
114
|
+
if not single_image_detector:
|
|
115
|
+
single_image_detector = ObjectDetector(self.api, self.logger)
|
|
116
|
+
self.single_image_detectors[identifier] = single_image_detector
|
|
117
|
+
|
|
118
|
+
single_image_detector.initialize(model, precision)
|
|
119
|
+
|
|
120
|
+
detections = await single_image_detector.detect_single(image_path, metadata)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"detections": detections,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def __main__():
|
|
128
|
+
return CoreML
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from typing import Any, Optional, cast
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import coremltools as ct
|
|
8
|
+
import numpy as np
|
|
9
|
+
import requests
|
|
10
|
+
from camera_ui_python_types import Detection, ImageMetadata, ObjectClass, PluginAPI, PluginLogger
|
|
11
|
+
from detector_defaults import available_models, available_precision
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from utils import parse_labels
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ObjectDetector:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
api: PluginAPI,
|
|
20
|
+
logger: PluginLogger,
|
|
21
|
+
):
|
|
22
|
+
self.api = api
|
|
23
|
+
self.logger = logger
|
|
24
|
+
self.config = self.api.config_service.all()
|
|
25
|
+
|
|
26
|
+
model_version = "v1"
|
|
27
|
+
self.model_path = os.path.join(f"{self.api.storage_path}/models/{model_version}")
|
|
28
|
+
|
|
29
|
+
self.initialized = False
|
|
30
|
+
self.minThreshold = 0.2
|
|
31
|
+
self.executor = ThreadPoolExecutor(max_workers=8)
|
|
32
|
+
|
|
33
|
+
self.selected_model = None
|
|
34
|
+
self.selected_precision = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def input_width(self) -> int:
|
|
38
|
+
return self.input_size[0]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def input_height(self) -> int:
|
|
42
|
+
return self.input_size[1]
|
|
43
|
+
|
|
44
|
+
def initialize(self, model: Optional[str] = None, precision: Optional[str] = None) -> None:
|
|
45
|
+
if not self.initialized:
|
|
46
|
+
self.initialized = True
|
|
47
|
+
|
|
48
|
+
self.logger.log("Initializing CoreML Object Detector...")
|
|
49
|
+
|
|
50
|
+
self.selected_model = model if model else self.config.get("model", "yolov9m_320")
|
|
51
|
+
self.selected_precision = precision if precision else self.config.get("precision", "FP16")
|
|
52
|
+
|
|
53
|
+
if self.selected_model not in available_models:
|
|
54
|
+
self.selected_model = "yolov9m_320"
|
|
55
|
+
|
|
56
|
+
if self.selected_precision not in available_precision:
|
|
57
|
+
self.selected_precision = "FP16"
|
|
58
|
+
|
|
59
|
+
yolo_model_name = self.selected_model.split("_")[0]
|
|
60
|
+
model_file = None
|
|
61
|
+
|
|
62
|
+
files = [
|
|
63
|
+
f"coreml_{self.selected_model}/{self.selected_precision}/{yolo_model_name}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
|
64
|
+
f"coreml_{self.selected_model}/{self.selected_precision}/{yolo_model_name}.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
|
65
|
+
f"coreml_{self.selected_model}/{self.selected_precision}/{yolo_model_name}.mlpackage/Manifest.json",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
for f in files:
|
|
69
|
+
p = self.__downloadFile(
|
|
70
|
+
f"https://raw.githubusercontent.com/seydx/models/main/models/coreml/{f}", f
|
|
71
|
+
)
|
|
72
|
+
model_file = os.path.dirname(p)
|
|
73
|
+
|
|
74
|
+
self.model = ct.models.MLModel(model=model_file, compute_units=ct.ComputeUnit.ALL)
|
|
75
|
+
self.input_name: str = self.model.get_spec().description.input[0].name # type: ignore
|
|
76
|
+
|
|
77
|
+
modelspec: Any = self.model.get_spec()
|
|
78
|
+
inputdesc = modelspec.description.input[0] # type: ignore
|
|
79
|
+
inputheight: Any = inputdesc.type.imageType.height
|
|
80
|
+
inputwidth: Any = inputdesc.type.imageType.width
|
|
81
|
+
|
|
82
|
+
self.input_size: tuple[int, int] = (inputwidth, inputheight)
|
|
83
|
+
self.labels = parse_labels(modelspec.description.metadata.userDefined) # type: ignore
|
|
84
|
+
|
|
85
|
+
self.logger.log("CoreML Object Detector initialized")
|
|
86
|
+
|
|
87
|
+
async def detect(self, image: Image.Image) -> tuple[list[int], list[float], np.ndarray[Any, Any]]:
|
|
88
|
+
if not self.initialized:
|
|
89
|
+
raise Exception("CoreML Object Detector not initialized")
|
|
90
|
+
|
|
91
|
+
out_dict = await asyncio.get_event_loop().run_in_executor(self.executor, self.__model_predict, image)
|
|
92
|
+
|
|
93
|
+
coordinatesList = out_dict["coordinates"]
|
|
94
|
+
confidenceList = out_dict["confidence"]
|
|
95
|
+
|
|
96
|
+
indices = np.argmax(confidenceList, axis=1)
|
|
97
|
+
maxConfidences = confidenceList[np.arange(len(confidenceList)), indices]
|
|
98
|
+
valid = maxConfidences >= self.minThreshold
|
|
99
|
+
|
|
100
|
+
if not np.any(valid):
|
|
101
|
+
return [], [], np.array([])
|
|
102
|
+
|
|
103
|
+
coordinates = coordinatesList[valid]
|
|
104
|
+
class_ids = indices[valid]
|
|
105
|
+
scores = maxConfidences[valid]
|
|
106
|
+
|
|
107
|
+
x_center = self.__torelative(coordinates[:, 0], self.input_width)
|
|
108
|
+
y_center = self.__torelative(coordinates[:, 1], self.input_height)
|
|
109
|
+
width = self.__torelative(coordinates[:, 2], self.input_width)
|
|
110
|
+
height = self.__torelative(coordinates[:, 3], self.input_height)
|
|
111
|
+
|
|
112
|
+
xmin = np.maximum(0, x_center - width / 2)
|
|
113
|
+
ymin = np.maximum(0, y_center - height / 2)
|
|
114
|
+
xmax = np.minimum(self.input_width, x_center + width / 2)
|
|
115
|
+
ymax = np.minimum(self.input_height, y_center + height / 2)
|
|
116
|
+
|
|
117
|
+
boxes = np.vstack([xmin, ymin, xmax, ymax]).T
|
|
118
|
+
# dets = np.hstack([boxes, scores.reshape(-1, 1), class_ids.reshape(-1, 1)])
|
|
119
|
+
|
|
120
|
+
return class_ids.tolist(), scores.tolist(), boxes
|
|
121
|
+
|
|
122
|
+
async def detect_single(self, image_path: str, metadata: ImageMetadata) -> list[Detection]:
|
|
123
|
+
image = Image.open(image_path)
|
|
124
|
+
image = image.convert("RGB").resize((self.input_width, self.input_height))
|
|
125
|
+
|
|
126
|
+
detections: list[Detection] = []
|
|
127
|
+
|
|
128
|
+
class_ids, scores, boxes = await self.detect(image)
|
|
129
|
+
|
|
130
|
+
for class_id, score, box in zip(class_ids, scores, boxes):
|
|
131
|
+
x1, y1, x2, y2 = box
|
|
132
|
+
|
|
133
|
+
detection: Detection = {
|
|
134
|
+
"id": str(uuid4()),
|
|
135
|
+
"label": cast(ObjectClass, self.labels[class_id]),
|
|
136
|
+
"confidence": score,
|
|
137
|
+
"boundingBox": (float(x1), float(y1), float(x2), float(y2)),
|
|
138
|
+
"inputWidth": self.input_width,
|
|
139
|
+
"inputHeight": self.input_height,
|
|
140
|
+
"origWidth": metadata["width"],
|
|
141
|
+
"origHeight": metadata["height"],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
detections.append(detection)
|
|
145
|
+
|
|
146
|
+
image.close()
|
|
147
|
+
|
|
148
|
+
return detections
|
|
149
|
+
|
|
150
|
+
def __torelative(self, values: int, size: int) -> int:
|
|
151
|
+
return values * size
|
|
152
|
+
|
|
153
|
+
def __model_predict(self, image: Image.Image) -> Any:
|
|
154
|
+
return self.model.predict({self.input_name: image}) # type: ignore
|
|
155
|
+
|
|
156
|
+
def __downloadFile(self, url: str, filename: str) -> str:
|
|
157
|
+
try:
|
|
158
|
+
fullpath = os.path.join(self.model_path, filename)
|
|
159
|
+
|
|
160
|
+
if os.path.isfile(fullpath):
|
|
161
|
+
return fullpath
|
|
162
|
+
|
|
163
|
+
tmp = fullpath + ".tmp"
|
|
164
|
+
|
|
165
|
+
self.logger.debug("Creating directory for", tmp)
|
|
166
|
+
|
|
167
|
+
os.makedirs(os.path.dirname(fullpath), exist_ok=True)
|
|
168
|
+
|
|
169
|
+
self.logger.debug("Downloading", url)
|
|
170
|
+
|
|
171
|
+
response = requests.get(url, stream=True)
|
|
172
|
+
|
|
173
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
174
|
+
raise Exception(f"Error downloading {url}: {response.status_code}")
|
|
175
|
+
|
|
176
|
+
read = 0
|
|
177
|
+
|
|
178
|
+
with open(tmp, "wb") as f:
|
|
179
|
+
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
|
180
|
+
if chunk:
|
|
181
|
+
read += len(chunk)
|
|
182
|
+
self.logger.debug("Downloaded", read, "bytes")
|
|
183
|
+
f.write(chunk)
|
|
184
|
+
|
|
185
|
+
os.rename(tmp, fullpath)
|
|
186
|
+
|
|
187
|
+
return fullpath
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.error("Error downloading", url, e)
|
|
190
|
+
raise
|
package/src/utils.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_label_contents(contents: str) -> dict[int, Any]:
|
|
7
|
+
lines = contents.split(",")
|
|
8
|
+
ret: dict[int, Any] = {}
|
|
9
|
+
|
|
10
|
+
for row_number, content in enumerate(lines):
|
|
11
|
+
pair = re.split(r"[:\s]+", content.strip(), maxsplit=1)
|
|
12
|
+
if len(pair) == 2 and pair[0].strip().isdigit():
|
|
13
|
+
ret[int(pair[0])] = pair[1].strip()
|
|
14
|
+
else:
|
|
15
|
+
ret[row_number] = content.strip()
|
|
16
|
+
|
|
17
|
+
return ret
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_labels(userDefined: dict[str, Any]) -> list[str]:
|
|
21
|
+
yolo = userDefined.get("names") or userDefined.get("yolo.names")
|
|
22
|
+
|
|
23
|
+
if yolo:
|
|
24
|
+
j = ast.literal_eval(yolo)
|
|
25
|
+
ret = []
|
|
26
|
+
for _, v in j.items():
|
|
27
|
+
ret.append(v)
|
|
28
|
+
|
|
29
|
+
return ret
|
|
30
|
+
|
|
31
|
+
classes = userDefined.get("classes")
|
|
32
|
+
|
|
33
|
+
# print("classes", classes)
|
|
34
|
+
|
|
35
|
+
if not classes:
|
|
36
|
+
raise Exception("no classes found in model metadata")
|
|
37
|
+
|
|
38
|
+
cont = parse_label_contents(classes)
|
|
39
|
+
ret: list[str] = []
|
|
40
|
+
|
|
41
|
+
for _, v in cont.items():
|
|
42
|
+
ret.append(v)
|
|
43
|
+
|
|
44
|
+
return ret
|