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

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.
@@ -1,43 +1,99 @@
1
1
  {
2
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"
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
+ }
31
96
  ]
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
97
  }
42
98
  }
43
99
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "CoreML",
3
3
  "name": "@camera.ui/camera-ui-coreml",
4
- "version": "0.0.21",
4
+ "version": "0.0.22",
5
5
  "description": "camera.ui coreml plugin",
6
6
  "author": "seydx (https://github.com/seydx/camera.ui)",
7
7
  "main": "./src/main.py",
@@ -21,7 +21,7 @@
21
21
  "url": "https://github.com/seydx/camera.ui/issues"
22
22
  },
23
23
  "engines": {
24
- "camera.ui": ">=0.0.34-alpha.1",
24
+ "camera.ui": ">=0.0.34-alpha.2",
25
25
  "node": ">=20.17.0"
26
26
  },
27
27
  "homepage": "https://github.com/seydx/camera.ui#readme",
package/requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
- coremltools==7.2
2
- camera-ui-python-types==0.1.93
1
+ coremltools==8.0
2
+ camera-ui-python-types==0.1.96
3
3
  requests==2.32.3
4
- Pillow==10.4.0
4
+ Pillow==11.0.0
5
+ aiohttp==3.10.10
@@ -1,162 +1,272 @@
1
+ # camera_detector.py
1
2
  import asyncio
2
- from typing import Any, Optional, cast
3
+ from typing import Optional, cast
3
4
  from uuid import uuid4
4
5
 
5
6
  from camera_ui_python_types import (
6
7
  CameraDevice,
8
+ CameraPropertyObservableObject,
7
9
  CameraStorage,
8
10
  Detection,
9
11
  LoggerService,
10
- MotionState,
11
12
  ObjectClass,
12
13
  PluginAPI,
13
14
  )
15
+
16
+ from detector_defaults import available_models, default_model
14
17
  from object_detector import ObjectDetector
15
18
  from plugin_typings import CameraStorageValues
16
19
 
17
- background_tasks = set[asyncio.Task[Any]]()
18
-
19
20
 
20
21
  class CameraDetector:
21
22
  def __init__(
22
23
  self,
23
24
  camera: CameraDevice,
24
- object_detector: ObjectDetector,
25
25
  api: PluginAPI,
26
26
  logger: LoggerService,
27
- has_motion: list[str],
28
27
  ) -> None:
29
28
  super().__init__()
30
29
 
31
30
  self.camera = camera
32
31
  self.api = api
33
- self.logger = logger
34
- self.object_detector = object_detector
32
+ self.camera_logger = camera.logger
33
+
34
+ self.object_detector = ObjectDetector(self.api, self.camera_logger)
35
35
 
36
- self.has_motion = has_motion
37
36
  self.started = False
38
37
  self.closed = False
39
- self.frame_generation_task: Optional[asyncio.Task[Any]] = None
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
40
52
 
41
53
  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
54
 
45
- def start(self):
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):
46
124
  if not self.started:
47
125
  self.started = True
48
126
  self.closed = False
49
127
 
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
- )
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")
55
132
 
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
- )
133
+ self.detection_task = asyncio.create_task(self.__start_detection())
62
134
 
63
135
  def close(self):
64
- if self.started and not self.closed:
136
+ if not self.closed:
65
137
  self.started = False
66
138
  self.closed = True
67
139
 
68
- self.logger.log(f"Stopping object detection for camera {self.camera.name}...")
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
69
149
 
70
- if self.frame_generation_task is not None:
71
- self.frame_generation_task.cancel()
72
- self.frame_generation_task = None
150
+ self.object_detector.close()
73
151
 
74
- if self.camera.name in self.has_motion:
75
- self.has_motion.remove(self.camera.name)
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()
76
160
 
77
- def restart(self):
78
- self.logger.log(f"Restarting object detection for camera {self.camera.name}...")
161
+ async def __restart(self) -> None:
162
+ if self.restarting or self.closed:
163
+ return
79
164
 
80
- self.close()
165
+ self.restarting = True
81
166
 
82
- if self.camera.connected:
83
- self.start()
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()
84
174
 
85
175
  async def __detect(self):
86
- while not self.closed:
87
- try:
88
- async for frame in self.camera.get_motion_frames():
89
- if self.closed:
90
- break
91
-
92
- frame_image = await frame.to_image(
93
- {
94
- "format": {"to": "rgb"},
95
- "resize": {
96
- "width": self.object_detector.input_width,
97
- "height": self.object_detector.input_height,
98
- },
99
- }
100
- )
101
-
102
- detections: list[Detection] = []
103
-
104
- class_ids, scores, boxes = await self.object_detector.detect(frame_image["image"])
105
-
106
- for class_id, score, box in zip(class_ids, scores, boxes):
107
- x1, y1, x2, y2 = box
108
-
109
- detection: Detection = {
110
- "id": str(uuid4()),
111
- "label": cast(ObjectClass, self.object_detector.labels[class_id]),
112
- "confidence": score,
113
- "boundingBox": (float(x1), float(y1), float(x2), float(y2)),
114
- "inputWidth": self.object_detector.input_width,
115
- "inputHeight": self.object_detector.input_height,
116
- "origWidth": frame.metadata["origWidth"],
117
- "origHeight": frame.metadata["origHeight"],
118
- }
119
-
120
- detections.append(detection)
121
-
122
- await self.camera.update_state("object", {"detections": detections})
123
- except asyncio.CancelledError:
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:
124
186
  break
125
- except Exception as error:
126
- self.logger.error(self.camera.name, "Error generating motion frames", error)
127
- self.logger.log(self.camera.name, "Restarting object detection in 5 seconds...")
128
- await asyncio.sleep(5)
129
- continue
130
187
 
131
- def __on_motion_detected(self, state: MotionState) -> None:
132
- motion_detected = bool(state.get("state", False) or len(state["detections"]) > 0)
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
133
196
 
134
- if motion_detected:
135
- if self.camera.name not in self.has_motion:
136
- self.has_motion.append(self.camera.name)
137
- elif self.camera.name in self.has_motion:
138
- self.has_motion.remove(self.camera.name)
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
+ )
139
206
 
140
- def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
141
- camera_storage: Optional[CameraStorage[CameraStorageValues]] = (
142
- self.api.storage_controller.get_camera_storage(self.camera.id)
143
- )
207
+ detections: list[Detection] = []
144
208
 
145
- if camera_storage is not None:
146
- return camera_storage
147
-
148
- return self.api.storage_controller.create_camera_storage(
149
- self,
150
- self.camera.id,
151
- {
152
- "active_object_detections": {
153
- "type": "string",
154
- "key": "active_object_detections",
155
- "title": "Active Object Detections",
156
- "description": "Active object detections",
157
- "readonly": True,
158
- "enum": [],
159
- "onGet": lambda: self.has_motion,
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"],
160
223
  }
161
- },
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
+ )
162
270
  )
271
+
272
+ return camera_storage
@@ -1,32 +1,86 @@
1
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
- "yolov10l",
29
- "yolov10l_320",
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",
30
80
  ]
31
81
 
32
82
  available_precision = ["INT8", "FP16"]
83
+
84
+ default_model = "yolov9m_320 - FP16"
85
+
86
+ model_version = "v1"
package/src/main.py CHANGED
@@ -1,6 +1,8 @@
1
+ # main.py
2
+ import asyncio
3
+ import os
1
4
  from typing import Any, Union
2
5
 
3
- from camera_detector import CameraDetector
4
6
  from camera_ui_python_types import (
5
7
  CameraDevice,
6
8
  CameraExtension,
@@ -12,8 +14,11 @@ from camera_ui_python_types import (
12
14
  PluginAPI,
13
15
  RootSchema,
14
16
  )
15
- from detector_defaults import available_models, available_precision
17
+
18
+ from camera_detector import CameraDetector
19
+ from detector_defaults import available_models, default_model, model_version
16
20
  from object_detector import ObjectDetector
21
+ from plugin_typings import FormPayload
17
22
 
18
23
 
19
24
  class CoreML(ObjectDetectionPlugin):
@@ -22,10 +27,6 @@ class CoreML(ObjectDetectionPlugin):
22
27
  self.logger = logger
23
28
  self.camera_devices: dict[str, CameraDevice] = {}
24
29
  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
 
30
31
  self.api.on("finishLaunching", self.start)
31
32
  self.api.on("shutdown", self.stop)
@@ -33,45 +34,90 @@ class CoreML(ObjectDetectionPlugin):
33
34
  self.api.device_manager.on("cameraSelected", self.camera_selected)
34
35
  self.api.device_manager.on("cameraDeselected", self.camera_deselected)
35
36
 
37
+ self.object_detectors: dict[str, ObjectDetector] = {}
38
+
36
39
  async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
37
- self.logger.log(f"New camera added ({extension}): {camera.name}")
40
+ self.logger.log(f"Camera added: {camera.name}")
38
41
  self.camera_devices[camera.id] = camera
39
- self.create_detector(camera)
42
+ await self.create_detector(camera)
40
43
 
41
- def camera_deselected(self, id: str, extension: CameraExtension) -> None:
44
+ async def camera_deselected(self, id: str, extension: CameraExtension) -> None:
42
45
  camera = self.camera_devices.get(id)
43
46
 
44
47
  if camera:
45
- self.logger.log(f"Camera removed ({extension}): {camera.name}")
46
- self.remove_detector(self.camera_devices[id])
48
+ self.logger.log(f"Camera removed: {camera.name}")
49
+ self.remove_detector(camera)
47
50
  del self.camera_devices[id]
48
51
 
49
- def start(self) -> None:
50
- self.logger.log("Plugin Started")
51
-
52
- self.object_detector.initialize()
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()])
53
55
 
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")
56
+ async def stop(self) -> None:
57
+ self.logger.log("Plugin stopped")
59
58
 
60
59
  detectors_copy = list(self.detectors.values())
61
60
 
62
61
  for detector in detectors_copy:
63
- self.remove_detector(detector.camera)
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")
64
105
 
65
- async def onFormSubmit(self, action_id: str, payload: Any) -> Union[FormSubmitResponse, None]:
66
- print("Form submitted", action_id, payload)
106
+ return {
107
+ "toast": {
108
+ "message": "All models removed!",
109
+ "type": "success",
110
+ }
111
+ }
67
112
 
68
113
  def configureCameras(self, cameras: list[CameraDevice]) -> None:
69
114
  for camera in cameras:
70
115
  self.camera_devices[camera.id] = camera
71
116
 
72
- def create_detector(self, camera: CameraDevice) -> None:
73
- detector = CameraDetector(camera, self.object_detector, self.api, self.logger, self.has_motion)
117
+ async def create_detector(self, camera: CameraDevice) -> None:
118
+ detector = CameraDetector(camera, self.api, self.logger)
74
119
  self.detectors[camera.id] = detector
120
+ await detector.initialize()
75
121
 
76
122
  def remove_detector(self, camera: CameraDevice) -> None:
77
123
  detector = self.detectors[camera.id]
@@ -86,38 +132,30 @@ class CoreML(ObjectDetectionPlugin):
86
132
  "models": {
87
133
  "type": "string",
88
134
  "title": "Model",
89
- "description": "Model to use for object detection",
135
+ "description": "Model for object detection",
90
136
  "required": True,
91
- "defaultValue": "yolov9m_320",
137
+ "defaultValue": default_model,
92
138
  "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
- },
139
+ }
102
140
  }
103
141
  }
104
142
 
105
143
  async def detectObjects(
106
144
  self, image_path: str, metadata: ImageMetadata, config: dict[str, Any]
107
145
  ) -> ObjectDetectionPluginResponse:
108
- model = config.get("models")
109
- precision = config.get("precision")
146
+ model_name: str = config.get("models", default_model)
147
+
148
+ object_detector = self.object_detectors.get(model_name)
110
149
 
111
- identifier = f"{model}_{precision}"
112
- single_image_detector = self.single_image_detectors.get(identifier)
150
+ if not object_detector:
151
+ object_detector = ObjectDetector(self.api, self.logger)
152
+ self.object_detectors[model_name] = object_detector
113
153
 
114
- if not single_image_detector:
115
- single_image_detector = ObjectDetector(self.api, self.logger)
116
- self.single_image_detectors[identifier] = single_image_detector
154
+ await object_detector.initialize(model_name)
117
155
 
118
- single_image_detector.initialize(model, precision)
156
+ detections = await object_detector.detect_single(image_path, metadata)
119
157
 
120
- detections = await single_image_detector.detect_single(image_path, metadata)
158
+ print("Detections: ", len(detections))
121
159
 
122
160
  return {
123
161
  "detections": detections,
@@ -1,15 +1,25 @@
1
+ # object_detector.py
1
2
  import asyncio
2
3
  import os
4
+ from asyncio import Lock
3
5
  from concurrent.futures import ThreadPoolExecutor
4
6
  from typing import Any, Optional, cast
5
7
  from uuid import uuid4
6
8
 
9
+ import aiohttp
7
10
  import coremltools as ct
8
11
  import numpy as np
9
- import requests
10
- from camera_ui_python_types import Detection, ImageMetadata, LoggerService, ObjectClass, PluginAPI
11
- from detector_defaults import available_models, available_precision
12
+ from camera_ui_python_types import (
13
+ CameraDevice,
14
+ Detection,
15
+ ImageMetadata,
16
+ LoggerService,
17
+ ObjectClass,
18
+ PluginAPI,
19
+ )
12
20
  from PIL import Image
21
+
22
+ from detector_defaults import available_models, available_precision, default_model, model_version
13
23
  from utils import parse_labels
14
24
 
15
25
 
@@ -18,12 +28,11 @@ class ObjectDetector:
18
28
  self,
19
29
  api: PluginAPI,
20
30
  logger: LoggerService,
31
+ camera: Optional[CameraDevice] = None,
21
32
  ):
22
33
  self.api = api
23
34
  self.logger = logger
24
- self.config = self.api.config_service.all()
25
35
 
26
- model_version = "v1"
27
36
  self.model_path = os.path.join(f"{self.api.storage_path}/models/{model_version}")
28
37
 
29
38
  self.initialized = False
@@ -33,6 +42,15 @@ class ObjectDetector:
33
42
  self.selected_model = None
34
43
  self.selected_precision = None
35
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
+
36
54
  @property
37
55
  def input_width(self) -> int:
38
56
  return self.input_size[0]
@@ -41,52 +59,82 @@ class ObjectDetector:
41
59
  def input_height(self) -> int:
42
60
  return self.input_size[1]
43
61
 
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...")
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")
49
66
 
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")
67
+ if not self.initialized:
68
+ self.initialized = True
69
+ self.__downloading = True
52
70
 
53
- if self.selected_model not in available_models:
54
- self.selected_model = "yolov9m_320"
71
+ self.selected_model, self.selected_precision = model_name.split(" - ")
55
72
 
56
- if self.selected_precision not in available_precision:
57
- self.selected_precision = "FP16"
73
+ if self.selected_model not in available_models:
74
+ self.selected_model = default_model.split(" - ")[0]
58
75
 
59
- yolo_model_name = self.selected_model.split("_")[0]
60
- model_file = None
76
+ if self.selected_precision not in available_precision:
77
+ self.selected_precision = default_model.split(" - ")[1]
61
78
 
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
- ]
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
+ )
67
83
 
68
- for f in files:
69
- p = self.__downloadFile(
70
- f"https://raw.githubusercontent.com/seydx/models/main/models/coreml/{f}", f
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)
71
100
  )
72
- model_file = os.path.dirname(p)
101
+ self.input_name: str = self.model.get_spec().description.input[0].name # type: ignore
73
102
 
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
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
76
107
 
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
108
+ self.input_size: tuple[int, int] = (inputwidth, inputheight)
109
+ self.labels = parse_labels(modelspec.description.metadata.userDefined) # type: ignore
81
110
 
82
- self.input_size: tuple[int, int] = (inputwidth, inputheight)
83
- self.labels = parse_labels(modelspec.description.metadata.userDefined) # type: ignore
111
+ self.__downloading = False
84
112
 
85
- self.logger.log("CoreML Object Detector initialized")
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)
86
134
 
87
135
  async def detect(self, image: Image.Image) -> tuple[list[int], list[float], np.ndarray[Any, Any]]:
88
136
  if not self.initialized:
89
- raise Exception("CoreML Object Detector not initialized")
137
+ return [], [], np.array([])
90
138
 
91
139
  out_dict = await asyncio.get_event_loop().run_in_executor(self.executor, self.__model_predict, image)
92
140
 
@@ -115,7 +163,6 @@ class ObjectDetector:
115
163
  ymax = np.minimum(self.input_height, y_center + height / 2)
116
164
 
117
165
  boxes = np.vstack([xmin, ymin, xmax, ymax]).T
118
- # dets = np.hstack([boxes, scores.reshape(-1, 1), class_ids.reshape(-1, 1)])
119
166
 
120
167
  return class_ids.tolist(), scores.tolist(), boxes
121
168
 
@@ -153,38 +200,29 @@ class ObjectDetector:
153
200
  def __model_predict(self, image: Image.Image) -> Any:
154
201
  return self.model.predict({self.input_name: image}) # type: ignore
155
202
 
156
- def __downloadFile(self, url: str, filename: str) -> str:
157
- try:
158
- fullpath = os.path.join(self.model_path, filename)
203
+ async def __downloadFile(self, url: str, filename: str) -> None:
204
+ fullpath = os.path.join(self.model_path, filename)
159
205
 
160
- if os.path.isfile(fullpath):
161
- return fullpath
206
+ if os.path.isfile(fullpath):
207
+ return
162
208
 
163
- tmp = fullpath + ".tmp"
209
+ tmp = fullpath + ".tmp"
164
210
 
165
- self.logger.debug("Creating directory for", tmp)
211
+ os.makedirs(os.path.dirname(fullpath), exist_ok=True)
166
212
 
167
- os.makedirs(os.path.dirname(fullpath), exist_ok=True)
213
+ self.logger.debug("Downloading", url)
168
214
 
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}")
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}")
175
218
 
176
219
  read = 0
177
220
 
178
221
  with open(tmp, "wb") as f:
179
- for chunk in response.iter_content(chunk_size=1024 * 1024):
222
+ async for chunk in response.content.iter_chunked(1024 * 1024):
180
223
  if chunk:
181
224
  read += len(chunk)
182
- self.logger.debug("Downloaded", read, "bytes")
225
+ self.logger.debug("Downloaded", read, "Bytes")
183
226
  f.write(chunk)
184
227
 
185
- os.rename(tmp, fullpath)
186
-
187
- return fullpath
188
- except Exception as e:
189
- self.logger.error("Error downloading", url, e)
190
- raise
228
+ os.rename(tmp, fullpath)
@@ -1,5 +1,10 @@
1
- from typing import TypedDict
1
+ from typing import Optional, TypedDict
2
2
 
3
3
 
4
4
  class CameraStorageValues(TypedDict):
5
- active_object_detections: list[str]
5
+ model: str
6
+
7
+
8
+ class FormPayload(TypedDict):
9
+ model: str
10
+ confirm: Optional[str]