@camera.ui/camera-ui-coreml 0.0.22 → 0.0.23

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/bundle.zip ADDED
Binary file
package/package.json CHANGED
@@ -1,28 +1,29 @@
1
1
  {
2
2
  "displayName": "CoreML",
3
3
  "name": "@camera.ui/camera-ui-coreml",
4
- "version": "0.0.22",
4
+ "version": "0.0.23",
5
5
  "description": "camera.ui coreml plugin",
6
6
  "author": "seydx (https://github.com/seydx/camera.ui)",
7
- "main": "./src/main.py",
8
- "type": "module",
7
+ "main": "./dist/main.py",
8
+ "type": "commonjs",
9
9
  "scripts": {
10
+ "bundle": "npm run format && npm run lint && cui bundle",
10
11
  "format": "ruff format",
11
12
  "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"
13
+ "lint": "ruff check",
14
+ "lint:fix": "ruff check --fix",
15
+ "prepublishOnly": "node -e \"if(!process.env.SAFE_PUBLISH){console.error('Error: Please use @camera.ui/cli to publish the plugin:\\n npm run publish:alpha\\n npm run publish:beta\\n npm run publish:latest\\n');process.exit(1)}\"",
16
+ "publish:alpha": "cui publish --alpha",
17
+ "publish:beta": "cui publish --beta",
18
+ "publish:latest": "cui publish --latest",
19
+ "update": "updates --update ./"
19
20
  },
20
21
  "bugs": {
21
22
  "url": "https://github.com/seydx/camera.ui/issues"
22
23
  },
23
24
  "engines": {
24
25
  "camera.ui": ">=0.0.34-alpha.2",
25
- "node": ">=20.17.0"
26
+ "node": ">=20.18.0"
26
27
  },
27
28
  "homepage": "https://github.com/seydx/camera.ui#readme",
28
29
  "keywords": [
@@ -40,6 +41,7 @@
40
41
  "camera.ui": {
41
42
  "pythonVersion": "3.11",
42
43
  "extension": "objectDetection",
43
- "dependencies": []
44
+ "dependencies": [],
45
+ "bundled": true
44
46
  }
45
- }
47
+ }
package/CHANGELOG.md DELETED
@@ -1,8 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- # Contributing
package/LICENSE.md DELETED
@@ -1,22 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- # @camera.ui/camera-ui-coreml
@@ -1,99 +0,0 @@
1
- {
2
- "schema": {
3
- "items": {
4
- "type": "object",
5
- "title": "Remove Models",
6
- "opened": true,
7
- "properties": {
8
- "model": {
9
- "type": "string",
10
- "title": "Model",
11
- "description": "Model to use for object detection",
12
- "required": true,
13
- "store": false,
14
- "defaultValue": "yolov9m_320 - FP16",
15
- "enum": [
16
- "yolo3-tinyu - INT8",
17
- "yolo3-tinyu_320 - INT8",
18
- "yolo3-tinyu - FP16",
19
- "yolo3-tinyu_320 - FP16",
20
- "yolov5nu - INT8",
21
- "yolov5nu_320 - INT8",
22
- "yolov5mu - INT8",
23
- "yolov5mu_320 - INT8",
24
- "yolov5nu - FP16",
25
- "yolov5nu_320 - FP16",
26
- "yolov5mu - FP16",
27
- "yolov5mu_320 - FP16",
28
- "yolov8n - INT8",
29
- "yolov8n_320 - INT8",
30
- "yolov8s - INT8",
31
- "yolov8s_320 - INT8",
32
- "yolov8m - INT8",
33
- "yolov8m_320 - INT8",
34
- "yolov8n - FP16",
35
- "yolov8n_320 - FP16",
36
- "yolov8s - FP16",
37
- "yolov8s_320 - FP16",
38
- "yolov8m - FP16",
39
- "yolov8m_320 - FP16",
40
- "yolov9t - INT8",
41
- "yolov9t_320 - INT8",
42
- "yolov9s - INT8",
43
- "yolov9s_320 - INT8",
44
- "yolov9m - INT8",
45
- "yolov9m_320 - INT8",
46
- "yolov9c - INT8",
47
- "yolov9c_320 - INT8",
48
- "yolov9t - FP16",
49
- "yolov9t_320 - FP16",
50
- "yolov9s - FP16",
51
- "yolov9s_320 - FP16",
52
- "yolov9m - FP16",
53
- "yolov9m_320 - FP16",
54
- "yolov9c - FP16",
55
- "yolov9c_320 - FP16",
56
- "yolov10n - INT8",
57
- "yolov10n_320 - INT8",
58
- "yolov10s - INT8",
59
- "yolov10s_320 - INT8",
60
- "yolov10m - INT8",
61
- "yolov10m_320 - INT8",
62
- "yolov10l - INT8",
63
- "yolov10l_320 - INT8",
64
- "yolov10n - FP16",
65
- "yolov10n_320 - FP16",
66
- "yolov10s - FP16",
67
- "yolov10s_320 - FP16",
68
- "yolov10m - FP16",
69
- "yolov10m_320 - FP16",
70
- "yolov10l - FP16",
71
- "yolov10l_320 - FP16",
72
- "yolov11n - INT8",
73
- "yolov11n_320 - INT8",
74
- "yolov11s - INT8",
75
- "yolov11s_320 - INT8",
76
- "yolov11m - INT8",
77
- "yolov11m_320 - INT8",
78
- "yolov11l - INT8",
79
- "yolov11l_320 - INT8",
80
- "yolov11n - FP16",
81
- "yolov11n_320 - FP16",
82
- "yolov11s - FP16",
83
- "yolov11s_320 - FP16",
84
- "yolov11m - FP16",
85
- "yolov11m_320 - FP16",
86
- "yolov11l - FP16",
87
- "yolov11l_320 - FP16"
88
- ]
89
- }
90
- },
91
- "buttons": [
92
- {
93
- "label": "Remove",
94
- "onSubmit": "onRemove"
95
- }
96
- ]
97
- }
98
- }
99
- }
package/requirements.txt DELETED
@@ -1,5 +0,0 @@
1
- coremltools==8.0
2
- camera-ui-python-types==0.1.96
3
- requests==2.32.3
4
- Pillow==11.0.0
5
- aiohttp==3.10.10
@@ -1,272 +0,0 @@
1
- # camera_detector.py
2
- import asyncio
3
- from typing import Optional, cast
4
- from uuid import uuid4
5
-
6
- from camera_ui_python_types import (
7
- CameraDevice,
8
- CameraPropertyObservableObject,
9
- CameraStorage,
10
- Detection,
11
- LoggerService,
12
- ObjectClass,
13
- PluginAPI,
14
- )
15
-
16
- from detector_defaults import available_models, default_model
17
- from object_detector import ObjectDetector
18
- from plugin_typings import CameraStorageValues
19
-
20
-
21
- class CameraDetector:
22
- def __init__(
23
- self,
24
- camera: CameraDevice,
25
- api: PluginAPI,
26
- logger: LoggerService,
27
- ) -> None:
28
- super().__init__()
29
-
30
- self.camera = camera
31
- self.api = api
32
- self.camera_logger = camera.logger
33
-
34
- self.object_detector = ObjectDetector(self.api, self.camera_logger)
35
-
36
- self.started = False
37
- self.closed = False
38
- self.restarting = False
39
-
40
- self.initialized = False
41
-
42
- self.current_generator_type: Optional[bool] = None # True for extended, False for normal
43
- self.detection_task: Optional[asyncio.Task[None]] = None
44
-
45
- # Keep track of camera properties
46
- self.connected = False
47
- self.has_motion_detector = False
48
- self.extended_motion_detector = False
49
-
50
- # Debounce task
51
- self.debounce_task: Optional[asyncio.Task[None]] = None
52
-
53
- self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
54
-
55
- async def initialize(self) -> None:
56
- if self.initialized:
57
- return
58
-
59
- self.initialized = True
60
-
61
- # Initialize object detector
62
- try:
63
- model_name = self.camera_storage.values["model"]
64
- self.camera_logger.log(f"Initialising Object Detection with model: {model_name}")
65
- await self.object_detector.initialize(model_name)
66
- except Exception as error:
67
- self.camera_logger.error("Error initializing Object Detection", error)
68
- return
69
-
70
- if self.closed:
71
- return
72
-
73
- # Initialize property values
74
- self.connected = self.camera.connected
75
- self.has_motion_detector = self.camera.has_motion_detector
76
- self.extended_motion_detector = self.camera.extended_motion_detector
77
-
78
- # Subscribe to changes
79
- self.camera.on_connected.subscribe(self.on_camera_connected)
80
- self.camera.on_property_change("hasMotionDetector").subscribe(self.on_has_motion_detector_changed)
81
- self.camera.on_property_change("extendedMotionDetector").subscribe(
82
- self.on_extended_motion_detector_changed
83
- )
84
-
85
- # Start detection if initial conditions are met
86
- await self.check_and_start_or_stop_detection()
87
-
88
- def schedule_check(self):
89
- if self.debounce_task and not self.debounce_task.done():
90
- return # Debounce task already scheduled
91
-
92
- self.debounce_task = asyncio.create_task(self.debounce_check())
93
-
94
- async def debounce_check(self):
95
- await asyncio.sleep(0.1) # Debounce delay
96
- await self.check_and_start_or_stop_detection()
97
-
98
- def on_camera_connected(self, connected: bool) -> None:
99
- self.connected = connected
100
- self.schedule_check()
101
-
102
- def on_has_motion_detector_changed(self, state: CameraPropertyObservableObject) -> None:
103
- self.has_motion_detector: bool = state["new_state"]
104
- self.schedule_check()
105
-
106
- def on_extended_motion_detector_changed(self, state: CameraPropertyObservableObject) -> None:
107
- self.extended_motion_detector: bool = state["new_state"]
108
- self.schedule_check()
109
-
110
- async def check_and_start_or_stop_detection(self):
111
- if self.connected and self.has_motion_detector:
112
- if not self.started:
113
- await self.start()
114
- else:
115
- # Restart detection if the generator type has changed
116
- if self.current_generator_type != self.extended_motion_detector:
117
- self.close()
118
- await self.start()
119
- else:
120
- if self.started:
121
- self.close()
122
-
123
- async def start(self):
124
- if not self.started:
125
- self.started = True
126
- self.closed = False
127
-
128
- if self.extended_motion_detector:
129
- self.camera_logger.log("Starting Object Detection with Motion Frames")
130
- else:
131
- self.camera_logger.log("Starting Object Detection")
132
-
133
- self.detection_task = asyncio.create_task(self.__start_detection())
134
-
135
- def close(self):
136
- if not self.closed:
137
- self.started = False
138
- self.closed = True
139
-
140
- self.camera_logger.log("Stopping Object Detection")
141
-
142
- if self.detection_task:
143
- self.detection_task.cancel()
144
- self.detection_task = None
145
-
146
- if self.debounce_task:
147
- self.debounce_task.cancel()
148
- self.debounce_task = None
149
-
150
- self.object_detector.close()
151
-
152
- async def __start_detection(self) -> None:
153
- try:
154
- await self.__detect()
155
- except asyncio.CancelledError:
156
- pass
157
- except Exception as error:
158
- self.camera_logger.error("Error generating frames", error)
159
- await self.__restart()
160
-
161
- async def __restart(self) -> None:
162
- if self.restarting or self.closed:
163
- return
164
-
165
- self.restarting = True
166
-
167
- self.camera_logger.log("Restarting Object Detection in 2s...")
168
- await asyncio.sleep(2)
169
-
170
- self.restarting = False
171
-
172
- if self.connected and not self.closed and self.has_motion_detector:
173
- await self.__start_detection()
174
-
175
- async def __detect(self):
176
- # Choose the generator based on extendedMotionDetector
177
- if self.extended_motion_detector:
178
- generator = self.camera.get_motion_frames()
179
- else:
180
- generator = self.camera.get_frames()
181
-
182
- self.current_generator_type = self.extended_motion_detector
183
-
184
- async for frame in generator:
185
- if self.closed:
186
- break
187
-
188
- # Check if properties have changed
189
- if (
190
- self.connected != self.camera.connected
191
- or self.has_motion_detector != self.camera.has_motion_detector
192
- or self.extended_motion_detector != self.camera.extended_motion_detector
193
- ):
194
- await self.__restart()
195
- break
196
-
197
- frame_image = await frame.to_image(
198
- {
199
- "format": {"to": "rgb"},
200
- "resize": {
201
- "width": self.object_detector.input_width,
202
- "height": self.object_detector.input_height,
203
- },
204
- }
205
- )
206
-
207
- detections: list[Detection] = []
208
-
209
- class_ids, scores, boxes = await self.object_detector.detect(frame_image["image"])
210
-
211
- for class_id, score, box in zip(class_ids, scores, boxes):
212
- x1, y1, x2, y2 = box
213
-
214
- detection: Detection = {
215
- "id": str(uuid4()),
216
- "label": cast(ObjectClass, self.object_detector.labels[class_id]),
217
- "confidence": score,
218
- "boundingBox": (float(x1), float(y1), float(x2), float(y2)),
219
- "inputWidth": self.object_detector.input_width,
220
- "inputHeight": self.object_detector.input_height,
221
- "origWidth": frame.metadata["origWidth"],
222
- "origHeight": frame.metadata["origHeight"],
223
- }
224
-
225
- detections.append(detection)
226
-
227
- await self.camera.update_state("object", {"detections": detections})
228
-
229
- async def __default_settings(self) -> None:
230
- if self.camera_storage.values["model"] != default_model:
231
- await self.camera_storage.setValue("model", default_model)
232
-
233
- async def __change_model(self, new_model: str, old_model: str) -> None:
234
- if self.object_detector.downloading:
235
- self.camera_logger.log("Model is currently downloading, please wait")
236
- await self.camera_storage.setValue("model", old_model)
237
- return
238
-
239
- self.camera_logger.log(f"Model changed from {old_model} to {new_model}")
240
-
241
- await self.object_detector.reset()
242
- await self.object_detector.initialize(new_model)
243
-
244
- def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
245
- camera_storage: CameraStorage[CameraStorageValues] = (
246
- self.api.storage_controller.create_camera_storage(
247
- self,
248
- self.camera.id,
249
- {
250
- "model": {
251
- "type": "string",
252
- "key": "model",
253
- "title": "Model",
254
- "description": "Model for object detection",
255
- "store": True,
256
- "defaultValue": default_model,
257
- "enum": available_models,
258
- "required": True,
259
- "onSet": lambda new, old: self.__change_model(new, old),
260
- },
261
- "reset": {
262
- "type": "button",
263
- "title": "Default Settings",
264
- "key": "default",
265
- "description": "Reset settings to default",
266
- "onSet": lambda new, old: self.__default_settings(),
267
- },
268
- },
269
- )
270
- )
271
-
272
- return camera_storage
@@ -1,86 +0,0 @@
1
- available_models = [
2
- # YOLOv3
3
- "yolo3-tinyu - INT8",
4
- "yolo3-tinyu_320 - INT8",
5
- "yolo3-tinyu - FP16",
6
- "yolo3-tinyu_320 - FP16",
7
- # YOLOv5
8
- "yolov5nu - INT8",
9
- "yolov5nu_320 - INT8",
10
- "yolov5mu - INT8",
11
- "yolov5mu_320 - INT8",
12
- "yolov5nu - FP16",
13
- "yolov5nu_320 - FP16",
14
- "yolov5mu - FP16",
15
- "yolov5mu_320 - FP16",
16
- # YOLOv8
17
- "yolov8n - INT8",
18
- "yolov8n_320 - INT8",
19
- "yolov8s - INT8",
20
- "yolov8s_320 - INT8",
21
- "yolov8m - INT8",
22
- "yolov8m_320 - INT8",
23
- "yolov8n - FP16",
24
- "yolov8n_320 - FP16",
25
- "yolov8s - FP16",
26
- "yolov8s_320 - FP16",
27
- "yolov8m - FP16",
28
- "yolov8m_320 - FP16",
29
- # YOLOv9
30
- "yolov9t - INT8",
31
- "yolov9t_320 - INT8",
32
- "yolov9s - INT8",
33
- "yolov9s_320 - INT8",
34
- "yolov9m - INT8",
35
- "yolov9m_320 - INT8",
36
- "yolov9c - INT8",
37
- "yolov9c_320 - INT8",
38
- "yolov9t - FP16",
39
- "yolov9t_320 - FP16",
40
- "yolov9s - FP16",
41
- "yolov9s_320 - FP16",
42
- "yolov9m - FP16",
43
- "yolov9m_320 - FP16",
44
- "yolov9c - FP16",
45
- "yolov9c_320 - FP16",
46
- # YOLOv10
47
- "yolov10n - INT8",
48
- "yolov10n_320 - INT8",
49
- "yolov10s - INT8",
50
- "yolov10s_320 - INT8",
51
- "yolov10m - INT8",
52
- "yolov10m_320 - INT8",
53
- "yolov10l - INT8",
54
- "yolov10l_320 - INT8",
55
- "yolov10n - FP16",
56
- "yolov10n_320 - FP16",
57
- "yolov10s - FP16",
58
- "yolov10s_320 - FP16",
59
- "yolov10m - FP16",
60
- "yolov10m_320 - FP16",
61
- "yolov10l - FP16",
62
- "yolov10l_320 - FP16",
63
- # YOLOv11
64
- "yolov11n - INT8",
65
- "yolov11n_320 - INT8",
66
- "yolov11s - INT8",
67
- "yolov11s_320 - INT8",
68
- "yolov11m - INT8",
69
- "yolov11m_320 - INT8",
70
- "yolov11l - INT8",
71
- "yolov11l_320 - INT8",
72
- "yolov11n - FP16",
73
- "yolov11n_320 - FP16",
74
- "yolov11s - FP16",
75
- "yolov11s_320 - FP16",
76
- "yolov11m - FP16",
77
- "yolov11m_320 - FP16",
78
- "yolov11l - FP16",
79
- "yolov11l_320 - FP16",
80
- ]
81
-
82
- available_precision = ["INT8", "FP16"]
83
-
84
- default_model = "yolov9m_320 - FP16"
85
-
86
- model_version = "v1"
package/src/main.py DELETED
@@ -1,166 +0,0 @@
1
- # main.py
2
- import asyncio
3
- import os
4
- from typing import Any, Union
5
-
6
- from camera_ui_python_types import (
7
- CameraDevice,
8
- CameraExtension,
9
- FormSubmitResponse,
10
- ImageMetadata,
11
- LoggerService,
12
- ObjectDetectionPlugin,
13
- ObjectDetectionPluginResponse,
14
- PluginAPI,
15
- RootSchema,
16
- )
17
-
18
- from camera_detector import CameraDetector
19
- from detector_defaults import available_models, default_model, model_version
20
- from object_detector import ObjectDetector
21
- from plugin_typings import FormPayload
22
-
23
-
24
- class CoreML(ObjectDetectionPlugin):
25
- def __init__(self, logger: LoggerService, api: PluginAPI):
26
- self.api = api
27
- self.logger = logger
28
- self.camera_devices: dict[str, CameraDevice] = {}
29
- self.detectors: dict[str, CameraDetector] = {}
30
-
31
- self.api.on("finishLaunching", self.start)
32
- self.api.on("shutdown", self.stop)
33
-
34
- self.api.device_manager.on("cameraSelected", self.camera_selected)
35
- self.api.device_manager.on("cameraDeselected", self.camera_deselected)
36
-
37
- self.object_detectors: dict[str, ObjectDetector] = {}
38
-
39
- async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
40
- self.logger.log(f"Camera added: {camera.name}")
41
- self.camera_devices[camera.id] = camera
42
- await self.create_detector(camera)
43
-
44
- async def camera_deselected(self, id: str, extension: CameraExtension) -> None:
45
- camera = self.camera_devices.get(id)
46
-
47
- if camera:
48
- self.logger.log(f"Camera removed: {camera.name}")
49
- self.remove_detector(camera)
50
- del self.camera_devices[id]
51
-
52
- async def start(self) -> None:
53
- self.logger.log("Plugin started")
54
- await asyncio.gather(*[self.create_detector(camera) for camera in self.camera_devices.values()])
55
-
56
- async def stop(self) -> None:
57
- self.logger.log("Plugin stopped")
58
-
59
- detectors_copy = list(self.detectors.values())
60
-
61
- for detector in detectors_copy:
62
- detector.close()
63
- del self.detectors[detector.camera.id]
64
-
65
- async def onFormSubmit(self, action_id: str, payload: FormPayload) -> Union[FormSubmitResponse, None]:
66
- if action_id == "onRemove":
67
- response: FormSubmitResponse = {
68
- "schema": {
69
- "config": {
70
- "type": "object",
71
- "title": "Confirm",
72
- "required": True,
73
- "opened": True,
74
- "properties": {
75
- "confirm": {
76
- "type": "string",
77
- "description": "Please type 'REMOVE' to confirm",
78
- "required": True,
79
- }
80
- },
81
- "buttons": [
82
- {
83
- "label": "Remove",
84
- "onSubmit": "onConfirm",
85
- },
86
- ],
87
- }
88
- }
89
- }
90
-
91
- return response
92
- elif action_id == "onConfirm":
93
- if payload.get("confirm") == "REMOVE":
94
- model, precision = payload["model"].split(" - ")
95
- filename = f"coreml_{model}/{precision}"
96
-
97
- model_storage = os.path.join(f"{self.api.storage_path}/models/{model_version}")
98
- dir = os.path.join(model_storage, filename)
99
-
100
- if os.path.exists(dir):
101
- self.logger.attention(f"Removing model: {model} - {precision}")
102
- os.rmdir(dir)
103
- else:
104
- self.logger.debug(f"Model not found: {model} - {precision}, skipping removal")
105
-
106
- return {
107
- "toast": {
108
- "message": "All models removed!",
109
- "type": "success",
110
- }
111
- }
112
-
113
- def configureCameras(self, cameras: list[CameraDevice]) -> None:
114
- for camera in cameras:
115
- self.camera_devices[camera.id] = camera
116
-
117
- async def create_detector(self, camera: CameraDevice) -> None:
118
- detector = CameraDetector(camera, self.api, self.logger)
119
- self.detectors[camera.id] = detector
120
- await detector.initialize()
121
-
122
- def remove_detector(self, camera: CameraDevice) -> None:
123
- detector = self.detectors[camera.id]
124
-
125
- if detector:
126
- detector.close()
127
- del self.detectors[camera.id]
128
-
129
- def interfaceSchema(self) -> RootSchema | None:
130
- return {
131
- "schema": {
132
- "models": {
133
- "type": "string",
134
- "title": "Model",
135
- "description": "Model for object detection",
136
- "required": True,
137
- "defaultValue": default_model,
138
- "enum": available_models,
139
- }
140
- }
141
- }
142
-
143
- async def detectObjects(
144
- self, image_path: str, metadata: ImageMetadata, config: dict[str, Any]
145
- ) -> ObjectDetectionPluginResponse:
146
- model_name: str = config.get("models", default_model)
147
-
148
- object_detector = self.object_detectors.get(model_name)
149
-
150
- if not object_detector:
151
- object_detector = ObjectDetector(self.api, self.logger)
152
- self.object_detectors[model_name] = object_detector
153
-
154
- await object_detector.initialize(model_name)
155
-
156
- detections = await object_detector.detect_single(image_path, metadata)
157
-
158
- print("Detections: ", len(detections))
159
-
160
- return {
161
- "detections": detections,
162
- }
163
-
164
-
165
- def __main__():
166
- return CoreML
@@ -1,228 +0,0 @@
1
- # object_detector.py
2
- import asyncio
3
- import os
4
- from asyncio import Lock
5
- from concurrent.futures import ThreadPoolExecutor
6
- from typing import Any, Optional, cast
7
- from uuid import uuid4
8
-
9
- import aiohttp
10
- import coremltools as ct
11
- import numpy as np
12
- from camera_ui_python_types import (
13
- CameraDevice,
14
- Detection,
15
- ImageMetadata,
16
- LoggerService,
17
- ObjectClass,
18
- PluginAPI,
19
- )
20
- from PIL import Image
21
-
22
- from detector_defaults import available_models, available_precision, default_model, model_version
23
- from utils import parse_labels
24
-
25
-
26
- class ObjectDetector:
27
- def __init__(
28
- self,
29
- api: PluginAPI,
30
- logger: LoggerService,
31
- camera: Optional[CameraDevice] = None,
32
- ):
33
- self.api = api
34
- self.logger = logger
35
-
36
- self.model_path = os.path.join(f"{self.api.storage_path}/models/{model_version}")
37
-
38
- self.initialized = False
39
- self.minThreshold = 0.2
40
- self.executor = ThreadPoolExecutor(max_workers=8)
41
-
42
- self.selected_model = None
43
- self.selected_precision = None
44
-
45
- self.closed = False
46
- self.__downloading = False
47
-
48
- self.lock = Lock()
49
-
50
- @property
51
- def downloading(self) -> bool:
52
- return self.__downloading
53
-
54
- @property
55
- def input_width(self) -> int:
56
- return self.input_size[0]
57
-
58
- @property
59
- def input_height(self) -> int:
60
- return self.input_size[1]
61
-
62
- async def initialize(self, model_name: str) -> None:
63
- async with self.lock:
64
- if self.__downloading:
65
- raise Exception("Already downloading a model")
66
-
67
- if not self.initialized:
68
- self.initialized = True
69
- self.__downloading = True
70
-
71
- self.selected_model, self.selected_precision = model_name.split(" - ")
72
-
73
- if self.selected_model not in available_models:
74
- self.selected_model = default_model.split(" - ")[0]
75
-
76
- if self.selected_precision not in available_precision:
77
- self.selected_precision = default_model.split(" - ")[1]
78
-
79
- yolo_model_name = self.selected_model.split("_")[0]
80
- ml_package = (
81
- f"coreml_{self.selected_model}/{self.selected_precision}/{yolo_model_name}.mlpackage"
82
- )
83
-
84
- files = [
85
- f"{ml_package}/Data/com.apple.CoreML/weights/weight.bin",
86
- f"{ml_package}/Data/com.apple.CoreML/model.mlmodel",
87
- f"{ml_package}/Manifest.json",
88
- ]
89
-
90
- for f in files:
91
- url = f"https://raw.githubusercontent.com/seydx/models/main/models/coreml/{f}"
92
- await self.__downloadFile(url, f)
93
- if self.closed:
94
- return
95
-
96
- # Proceed with model loading and initialization
97
- loop = asyncio.get_event_loop()
98
- self.model = await loop.run_in_executor(
99
- None, ct.models.MLModel, os.path.join(self.model_path, ml_package)
100
- )
101
- self.input_name: str = self.model.get_spec().description.input[0].name # type: ignore
102
-
103
- modelspec: Any = self.model.get_spec()
104
- inputdesc = modelspec.description.input[0] # type: ignore
105
- inputheight: Any = inputdesc.type.imageType.height
106
- inputwidth: Any = inputdesc.type.imageType.width
107
-
108
- self.input_size: tuple[int, int] = (inputwidth, inputheight)
109
- self.labels = parse_labels(modelspec.description.metadata.userDefined) # type: ignore
110
-
111
- self.__downloading = False
112
-
113
- async def reset(self) -> None:
114
- async with self.lock:
115
- self.initialized = False
116
- if self.executor:
117
- self.executor.shutdown(wait=False)
118
- self.executor = ThreadPoolExecutor(max_workers=8)
119
-
120
- def close(self) -> None:
121
- self.closed = True
122
- asyncio.create_task(self.reset())
123
- if self.executor:
124
- self.executor.shutdown(wait=False)
125
- self.executor = None
126
-
127
- def cleanup_model(self, model_name: str) -> None:
128
- model, precision = model_name.split(" - ")
129
- filename = f"coreml_{model}/{precision}"
130
- dir = os.path.join(self.model_path, filename)
131
-
132
- if os.path.exists(dir):
133
- os.rmdir(dir)
134
-
135
- async def detect(self, image: Image.Image) -> tuple[list[int], list[float], np.ndarray[Any, Any]]:
136
- if not self.initialized:
137
- return [], [], np.array([])
138
-
139
- out_dict = await asyncio.get_event_loop().run_in_executor(self.executor, self.__model_predict, image)
140
-
141
- coordinatesList = out_dict["coordinates"]
142
- confidenceList = out_dict["confidence"]
143
-
144
- indices = np.argmax(confidenceList, axis=1)
145
- maxConfidences = confidenceList[np.arange(len(confidenceList)), indices]
146
- valid = maxConfidences >= self.minThreshold
147
-
148
- if not np.any(valid):
149
- return [], [], np.array([])
150
-
151
- coordinates = coordinatesList[valid]
152
- class_ids = indices[valid]
153
- scores = maxConfidences[valid]
154
-
155
- x_center = self.__torelative(coordinates[:, 0], self.input_width)
156
- y_center = self.__torelative(coordinates[:, 1], self.input_height)
157
- width = self.__torelative(coordinates[:, 2], self.input_width)
158
- height = self.__torelative(coordinates[:, 3], self.input_height)
159
-
160
- xmin = np.maximum(0, x_center - width / 2)
161
- ymin = np.maximum(0, y_center - height / 2)
162
- xmax = np.minimum(self.input_width, x_center + width / 2)
163
- ymax = np.minimum(self.input_height, y_center + height / 2)
164
-
165
- boxes = np.vstack([xmin, ymin, xmax, ymax]).T
166
-
167
- return class_ids.tolist(), scores.tolist(), boxes
168
-
169
- async def detect_single(self, image_path: str, metadata: ImageMetadata) -> list[Detection]:
170
- image = Image.open(image_path)
171
- image = image.convert("RGB").resize((self.input_width, self.input_height))
172
-
173
- detections: list[Detection] = []
174
-
175
- class_ids, scores, boxes = await self.detect(image)
176
-
177
- for class_id, score, box in zip(class_ids, scores, boxes):
178
- x1, y1, x2, y2 = box
179
-
180
- detection: Detection = {
181
- "id": str(uuid4()),
182
- "label": cast(ObjectClass, self.labels[class_id]),
183
- "confidence": score,
184
- "boundingBox": (float(x1), float(y1), float(x2), float(y2)),
185
- "inputWidth": self.input_width,
186
- "inputHeight": self.input_height,
187
- "origWidth": metadata["width"],
188
- "origHeight": metadata["height"],
189
- }
190
-
191
- detections.append(detection)
192
-
193
- image.close()
194
-
195
- return detections
196
-
197
- def __torelative(self, values: int, size: int) -> int:
198
- return values * size
199
-
200
- def __model_predict(self, image: Image.Image) -> Any:
201
- return self.model.predict({self.input_name: image}) # type: ignore
202
-
203
- async def __downloadFile(self, url: str, filename: str) -> None:
204
- fullpath = os.path.join(self.model_path, filename)
205
-
206
- if os.path.isfile(fullpath):
207
- return
208
-
209
- tmp = fullpath + ".tmp"
210
-
211
- os.makedirs(os.path.dirname(fullpath), exist_ok=True)
212
-
213
- self.logger.debug("Downloading", url)
214
-
215
- async with aiohttp.ClientSession() as session, session.get(url) as response:
216
- if response.status < 200 or response.status >= 300:
217
- raise Exception(f"Error downloading {url}: {response.status}")
218
-
219
- read = 0
220
-
221
- with open(tmp, "wb") as f:
222
- async for chunk in response.content.iter_chunked(1024 * 1024):
223
- if chunk:
224
- read += len(chunk)
225
- self.logger.debug("Downloaded", read, "Bytes")
226
- f.write(chunk)
227
-
228
- os.rename(tmp, fullpath)
@@ -1,10 +0,0 @@
1
- from typing import Optional, TypedDict
2
-
3
-
4
- class CameraStorageValues(TypedDict):
5
- model: str
6
-
7
-
8
- class FormPayload(TypedDict):
9
- model: str
10
- confirm: Optional[str]
package/src/utils.py DELETED
@@ -1,44 +0,0 @@
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