@camera.ui/camera-ui-opencv 0.0.21 → 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": "OpenCV",
3
3
  "name": "@camera.ui/camera-ui-opencv",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "description": "camera.ui opencv 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
- "camera.ui": ">=0.0.34-alpha.2",
25
- "node": ">=20.17.0"
25
+ "camera.ui": ">=0.0.34-alpha.4",
26
+ "node": ">=20.18.0"
26
27
  },
27
28
  "homepage": "https://github.com/seydx/camera.ui#readme",
28
29
  "keywords": [
@@ -42,6 +43,7 @@
42
43
  "dependencies": [],
43
44
  "options": {
44
45
  "extendedMotionDetection": true
45
- }
46
+ },
47
+ "bundled": true
46
48
  }
47
- }
49
+ }
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-opencv
@@ -1,3 +0,0 @@
1
- {
2
- "schema": {}
3
- }
package/logo.png DELETED
Binary file
package/requirements.txt DELETED
@@ -1,3 +0,0 @@
1
- numpy==1.26.4
2
- opencv-python-headless==4.10.0.84
3
- camera-ui-python-types==0.1.96
@@ -1,434 +0,0 @@
1
- import asyncio
2
- from concurrent.futures import ThreadPoolExecutor
3
- from typing import Any, Literal, Optional
4
-
5
- import cv2
6
- import numpy as np
7
- from camera_ui_python_types import (
8
- CameraDevice,
9
- CameraStorage,
10
- Detection,
11
- PluginAPI,
12
- )
13
-
14
- from detector_defaults import (
15
- available_models,
16
- default_area,
17
- default_area_bs,
18
- default_area_fd,
19
- default_blur,
20
- default_dilt,
21
- default_learning_rate,
22
- default_model,
23
- default_threshold,
24
- default_threshold_bs,
25
- )
26
- from opencv_utils import get_detections, get_detections_bs, get_detections_fd
27
- from plugin_typings import CameraStorageValues
28
-
29
-
30
- class CameraDetector:
31
- def __init__(self, api: PluginAPI, camera_device: CameraDevice) -> None:
32
- super().__init__()
33
-
34
- self.api = api
35
- self.camera_device = camera_device
36
- self.camera_logger = camera_device.logger
37
-
38
- self.started = False
39
- self.closed = False
40
- self.restarting = False
41
-
42
- self.initialized = False
43
-
44
- self.executor = None
45
- self.previous_frame: Optional[np.ndarray[Any, Any]] = None
46
-
47
- self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
48
-
49
- async def initialize(self) -> None:
50
- if self.initialized:
51
- return
52
-
53
- self.initialized = True
54
-
55
- async def on_connected(connected: bool) -> None:
56
- if connected:
57
- await self.start()
58
- else:
59
- self.close()
60
-
61
- await self.camera_device.on_connected.asubscribe(on_connected)
62
-
63
- async def start(self) -> None:
64
- if not self.started:
65
- self.started = True
66
- self.closed = False
67
-
68
- self.executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
69
-
70
- self.camera_logger.log(f"Starting motion detection ({self.__detector_model()})...")
71
-
72
- await self.__start_detection()
73
-
74
- def close(self) -> None:
75
- if not self.closed:
76
- self.started = False
77
- self.closed = True
78
-
79
- self.camera_logger.log("Stopping motion detection")
80
-
81
- if self.executor is not None:
82
- self.executor.shutdown(wait=False)
83
- self.executor = None
84
-
85
- async def __start_detection(self) -> None:
86
- try:
87
- await self.__detect()
88
- except asyncio.CancelledError:
89
- pass
90
- except Exception as error:
91
- self.camera_logger.error("Error generating frames", error)
92
- await self.__restart()
93
-
94
- async def __restart(self) -> None:
95
- if self.restarting or self.closed:
96
- return
97
-
98
- self.restarting = True
99
-
100
- self.camera_logger.log("Restarting motion detection in 2s...")
101
- await asyncio.sleep(2)
102
-
103
- self.restarting = False
104
-
105
- if self.camera_device.connected and not self.closed:
106
- await self.__start_detection()
107
-
108
- async def __detect(self) -> None:
109
- backSub: Optional[cv2.BackgroundSubtractorMOG2] = None
110
-
111
- async for frame in self.camera_device.get_frames():
112
- if self.closed:
113
- break
114
-
115
- frame_image = await frame.to_image(
116
- {"format": {"to": "gray"}, "resize": {"width": 640, "height": 0}}
117
- )
118
-
119
- image_array = np.array(frame_image["image"])
120
-
121
- detector_model = self.__detector_model()
122
-
123
- if detector_model == "Frame Difference":
124
- area = self.camera_storage.values["frame_difference"]["area"]
125
-
126
- if self.previous_frame is None:
127
- self.previous_frame = image_array
128
- continue
129
-
130
- dets = await asyncio.get_event_loop().run_in_executor(
131
- self.executor,
132
- get_detections_fd,
133
- self.previous_frame,
134
- image_array,
135
- area,
136
- )
137
-
138
- self.previous_frame = image_array
139
-
140
- elif detector_model == "Background Substraction":
141
- if backSub is None:
142
- backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
143
-
144
- threshold = self.camera_storage.values["background_substraction"]["threshold"]
145
- area = self.camera_storage.values["background_substraction"]["area"]
146
- learning_rate = self.camera_storage.values["background_substraction"]["learning_rate"]
147
-
148
- dets = await asyncio.get_event_loop().run_in_executor(
149
- self.executor,
150
- get_detections_bs,
151
- image_array,
152
- backSub,
153
- threshold,
154
- area,
155
- learning_rate,
156
- )
157
-
158
- else: # default
159
- blur = self.camera_storage.values["default"]["blur"]
160
- threshold = self.camera_storage.values["default"]["threshold"]
161
- area = self.camera_storage.values["default"]["area"]
162
- image_array = cv2.stackBlur(image_array, (blur, blur))
163
-
164
- if self.previous_frame is None:
165
- self.previous_frame = image_array
166
- continue
167
-
168
- dets = await asyncio.get_event_loop().run_in_executor(
169
- self.executor,
170
- get_detections,
171
- self.previous_frame,
172
- image_array,
173
- threshold,
174
- area,
175
- )
176
-
177
- self.previous_frame = image_array
178
-
179
- detections: list[Detection] = []
180
-
181
- for det in dets:
182
- (x1, y1, x2, y2) = det
183
-
184
- detection: Detection = {
185
- "label": "motion",
186
- "confidence": 1,
187
- "boundingBox": (x1, y1, x2, y2),
188
- "inputWidth": frame_image["info"]["width"],
189
- "inputHeight": frame_image["info"]["height"],
190
- "origWidth": frame.metadata["origWidth"],
191
- "origHeight": frame.metadata["origHeight"],
192
- }
193
-
194
- detections.append(detection)
195
-
196
- await self.camera_device.update_state("motion", {"detections": detections}, frame)
197
-
198
- def __detector_model(self) -> Literal["Frame Difference", "Background Substraction", "Default"]:
199
- return self.camera_storage.values["motion_detector"]
200
-
201
- async def __default_settings(self) -> None:
202
- if self.camera_storage.values["motion_detector"] != default_model:
203
- await self.camera_storage.setValue("motion_detector", default_model)
204
-
205
- async def __default_d_settings(self) -> None:
206
- if self.camera_storage.values["default"]["area"] != default_area:
207
- await self.camera_storage.setValue("default.area", default_area)
208
-
209
- if self.camera_storage.values["default"]["threshold"] != default_threshold:
210
- await self.camera_storage.setValue("default.threshold", default_threshold)
211
-
212
- if self.camera_storage.values["default"]["blur"] != default_blur:
213
- await self.camera_storage.setValue("default.blur", default_blur)
214
-
215
- if self.camera_storage.values["default"]["dilation"] != default_dilt:
216
- await self.camera_storage.setValue("default.dilation", default_dilt)
217
-
218
- async def __default_bs_settings(self) -> None:
219
- if self.camera_storage.values["background_substraction"]["area"] != default_area_bs:
220
- await self.camera_storage.setValue("background_substraction.area", default_area_bs)
221
-
222
- if self.camera_storage.values["background_substraction"]["threshold"] != default_threshold_bs:
223
- await self.camera_storage.setValue("background_substraction.threshold", default_threshold_bs)
224
-
225
- if self.camera_storage.values["background_substraction"]["learning_rate"] != default_learning_rate:
226
- await self.camera_storage.setValue("background_substraction.learning_rate", default_learning_rate)
227
-
228
- async def __default_fd_settings(self) -> None:
229
- if self.camera_storage.values["frame_difference"]["area"] != default_area_fd:
230
- await self.camera_storage.setValue("frame_difference.area", default_area_fd)
231
-
232
- def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
233
- camera_storage: CameraStorage[CameraStorageValues] = (
234
- self.api.storage_controller.create_camera_storage(
235
- self,
236
- self.camera_device.id,
237
- {
238
- "motion_detector": {
239
- "type": "string",
240
- "key": "motion_detector",
241
- "title": "Motion Detector",
242
- "description": "Select the motion detection model to use",
243
- "group": "Manage",
244
- "enum": available_models,
245
- "store": True,
246
- "defaultValue": default_model,
247
- "required": True,
248
- "onSet": lambda new, old: self.camera_logger.log(
249
- f"Motion detection model changed from {old} to {new}"
250
- ),
251
- },
252
- "default": {
253
- "type": "object",
254
- "key": "default",
255
- "title": "Default",
256
- "description": "Settings for the default model",
257
- "group": "Default",
258
- "opened": True,
259
- "properties": {
260
- "area": {
261
- "type": "number",
262
- "key": "area",
263
- "title": "Area",
264
- "description": "Minimum size of detected motion (pixels)",
265
- "store": True,
266
- "defaultValue": default_area,
267
- "minimum": 10,
268
- "maximum": 1000,
269
- "step": 1,
270
- "required": True,
271
- "onSet": lambda new, old: self.camera_logger.log(
272
- f"Default model motion area changed from {old} to {new}",
273
- ),
274
- },
275
- "threshold": {
276
- "type": "number",
277
- "key": "threshold",
278
- "title": "Threshold",
279
- "description": "Sensitivity of motion detection (higher = less sensitive)",
280
- "store": True,
281
- "defaultValue": default_threshold,
282
- "minimum": 1,
283
- "maximum": 255,
284
- "step": 1,
285
- "required": True,
286
- "onSet": lambda new, old: self.camera_logger.log(
287
- f"Default model threshold changed from {old} to {new}",
288
- ),
289
- },
290
- "blur": {
291
- "type": "number",
292
- "key": "blur",
293
- "title": "Blur",
294
- "description": "Gaussian blur radius to reduce noise",
295
- "store": True,
296
- "defaultValue": default_blur,
297
- "minimum": 1,
298
- "maximum": 21,
299
- "step": 1,
300
- "required": True,
301
- "onSet": lambda new, old: self.camera_logger.log(
302
- f"Default model blur radius changed from {old} to {new}",
303
- ),
304
- },
305
- "dilation": {
306
- "type": "number",
307
- "key": "dilation",
308
- "title": "Dilation",
309
- "description": "Expansion of detected motion areas",
310
- "store": True,
311
- "defaultValue": default_dilt,
312
- "minimum": 1,
313
- "maximum": 21,
314
- "step": 1,
315
- "required": True,
316
- "onSet": lambda new, old: self.camera_logger.log(
317
- f"Default model dilation size changed from {old} to {new}",
318
- ),
319
- },
320
- "reset": {
321
- "type": "button",
322
- "title": "Default Settings",
323
- "key": "default_d",
324
- "description": "Reset motion detection settings to default",
325
- "onSet": lambda new, old: self.__default_d_settings(),
326
- },
327
- },
328
- },
329
- "background_substraction": {
330
- "type": "object",
331
- "key": "background_substraction",
332
- "title": "Background Substraction",
333
- "description": "Settings for the background substraction model",
334
- "group": "Background Substraction",
335
- "opened": True,
336
- "properties": {
337
- "area": {
338
- "type": "number",
339
- "key": "area",
340
- "title": "Area",
341
- "description": "Minimum size of detected motion (pixels)",
342
- "store": True,
343
- "defaultValue": default_area_bs,
344
- "minimum": 10,
345
- "maximum": 1000,
346
- "step": 1,
347
- "required": True,
348
- "onSet": lambda new, old: self.camera_logger.log(
349
- f"Background substraction model area changed from {old} to {new}",
350
- ),
351
- },
352
- "threshold": {
353
- "type": "number",
354
- "key": "threshold",
355
- "title": "Threshold",
356
- "description": "Sensitivity of motion detection (higher = less sensitive)",
357
- "store": True,
358
- "defaultValue": default_threshold_bs,
359
- "minimum": 1,
360
- "maximum": 255,
361
- "step": 1,
362
- "required": True,
363
- "onSet": lambda new, old: self.camera_logger.log(
364
- f"Background substraction model threshold changed from {old} to {new}",
365
- ),
366
- },
367
- "learning_rate": {
368
- "type": "number",
369
- "key": "threshold",
370
- "title": "Learning Rate",
371
- "description": "Speed of background model adaptation (0-1, -1 for auto)",
372
- "store": True,
373
- "defaultValue": default_learning_rate,
374
- "minimum": -1,
375
- "maximum": 1,
376
- "step": 0.01,
377
- "required": True,
378
- "onSet": lambda new, old: self.camera_logger.log(
379
- f"Background substraction model threshold changed from {old} to {new}",
380
- ),
381
- },
382
- "reset": {
383
- "type": "button",
384
- "title": "Default Settings",
385
- "key": "default_bs",
386
- "description": "Reset motion detection settings to default",
387
- "onSet": lambda new, old: self.__default_bs_settings(),
388
- },
389
- },
390
- },
391
- "frame_difference": {
392
- "type": "object",
393
- "key": "frame_difference",
394
- "title": "Frame Difference",
395
- "description": "Settings for the frame difference model",
396
- "group": "Frame Difference",
397
- "opened": True,
398
- "properties": {
399
- "area": {
400
- "type": "number",
401
- "key": "area",
402
- "title": "Area",
403
- "description": "Minimum size of detected motion (pixels)",
404
- "store": True,
405
- "defaultValue": default_area_fd,
406
- "minimum": 10,
407
- "maximum": 1000,
408
- "step": 1,
409
- "required": True,
410
- "onSet": lambda new, old: self.camera_logger.log(
411
- f"Frame difference model area changed from {old} to {new}",
412
- ),
413
- },
414
- "reset": {
415
- "type": "button",
416
- "title": "Default Settings",
417
- "key": "default_fd",
418
- "description": "Reset motion detection settings to default",
419
- "onSet": lambda new, old: self.__default_fd_settings(),
420
- },
421
- },
422
- },
423
- "reset": {
424
- "type": "button",
425
- "title": "Default Settings",
426
- "key": "default_settings",
427
- "description": "Reset motion detection settings to default",
428
- "onSet": lambda new, old: self.__default_settings(),
429
- },
430
- },
431
- )
432
- )
433
-
434
- return camera_storage
@@ -1,16 +0,0 @@
1
- default_area = 350
2
- default_area_fd = 350
3
- default_area_bs = 350
4
-
5
- default_threshold = 50
6
- default_threshold_bs = 50
7
-
8
- default_blur = 11
9
- default_dilt = 15
10
-
11
- default_learning_rate = 0.9
12
-
13
- available_models = ["Frame Difference", "Background Substraction", "Default"]
14
-
15
-
16
- default_model = "Background Substraction"
package/src/main.py DELETED
@@ -1,341 +0,0 @@
1
- import asyncio
2
- import tempfile
3
- from concurrent.futures import ThreadPoolExecutor
4
- from typing import Any, Optional, Union
5
-
6
- import cv2
7
- import numpy as np
8
- from camera_ui_python_types import (
9
- CameraDevice,
10
- CameraExtension,
11
- FormSubmitResponse,
12
- LoggerService,
13
- MotionDetectionPlugin,
14
- MotionDetectionPluginResponse,
15
- PluginAPI,
16
- RootSchema,
17
- )
18
-
19
- from camera_detector import CameraDetector
20
- from detector_defaults import (
21
- default_area,
22
- default_area_bs,
23
- default_area_fd,
24
- default_blur,
25
- default_dilt,
26
- default_learning_rate,
27
- default_model,
28
- default_threshold,
29
- default_threshold_bs,
30
- )
31
- from opencv_utils import get_detections, get_detections_bs, get_detections_fd, merge_overlapping_boxes
32
-
33
-
34
- class OpenCV(MotionDetectionPlugin):
35
- def __init__(self, logger: LoggerService, api: PluginAPI):
36
- self.api = api
37
- self.logger = logger
38
- self.cameras: dict[str, CameraDevice] = {}
39
- self.detectors: dict[str, CameraDetector] = {}
40
-
41
- self.api.on("finishLaunching", self.start)
42
- self.api.on("shutdown", self.stop)
43
-
44
- self.api.device_manager.on("cameraSelected", self.camera_selected)
45
- self.api.device_manager.on("cameraDeselected", self.camera_deselected)
46
-
47
- async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
48
- self.logger.log(f"New camera added ({extension}): {camera.name}")
49
- self.cameras[camera.id] = camera
50
- await self.create_detector(camera)
51
-
52
- def camera_deselected(self, id: str, extension: CameraExtension) -> None:
53
- camera = self.cameras.get(id)
54
-
55
- if camera:
56
- self.logger.log(f"Camera removed ({extension}): {camera.name}")
57
- self.remove_detector(self.cameras[id])
58
- del self.cameras[id]
59
-
60
- async def start(self) -> None:
61
- self.logger.log("Plugin Started")
62
-
63
- await asyncio.gather(*[self.create_detector(camera) for camera in self.cameras.values()])
64
-
65
- def stop(self) -> None:
66
- self.logger.log("Plugin Stopped")
67
-
68
- detectors_copy = list(self.detectors.values())
69
-
70
- for detector in detectors_copy:
71
- self.remove_detector(detector.camera_device)
72
-
73
- async def onFormSubmit(self, action_id: str, payload: Any) -> Union[FormSubmitResponse, None]:
74
- print("Form submitted", action_id, payload)
75
-
76
- def configureCameras(self, cameras: list[CameraDevice]) -> None:
77
- for camera in cameras:
78
- self.cameras[camera.id] = camera
79
-
80
- async def create_detector(self, camera: CameraDevice) -> None:
81
- detector = CameraDetector(self.api, camera)
82
- self.detectors[camera.id] = detector
83
- await detector.initialize()
84
-
85
- def remove_detector(self, camera: CameraDevice) -> None:
86
- detector = self.detectors[camera.id]
87
-
88
- if detector:
89
- detector.close()
90
- del self.detectors[camera.id]
91
-
92
- def interfaceSchema(self) -> RootSchema:
93
- root_schema: RootSchema = {
94
- "schema": {
95
- "motion_detector": {
96
- "type": "string",
97
- "key": "motion_detector",
98
- "title": "Motion Detector",
99
- "description": "Select the motion detection model to use",
100
- "enum": ["Frame Difference", "Background Substraction", "Default"],
101
- "store": False,
102
- "defaultValue": default_model,
103
- "required": True,
104
- },
105
- "default": {
106
- "type": "object",
107
- "key": "default",
108
- "title": "Default",
109
- "description": "Settings for the default model",
110
- "group": "Default",
111
- "opened": True,
112
- "hidden": True,
113
- "properties": {
114
- "area": {
115
- "type": "number",
116
- "key": "area",
117
- "title": "Area",
118
- "description": "Minimum size of detected motion (pixels)",
119
- "store": False,
120
- "defaultValue": default_area,
121
- "minimum": 10,
122
- "maximum": 1000,
123
- "step": 1,
124
- "required": True,
125
- },
126
- "threshold": {
127
- "type": "number",
128
- "key": "threshold",
129
- "title": "Threshold",
130
- "description": "Sensitivity of motion detection (higher = less sensitive)",
131
- "store": False,
132
- "defaultValue": default_threshold,
133
- "minimum": 1,
134
- "maximum": 255,
135
- "step": 1,
136
- "required": True,
137
- },
138
- "blur": {
139
- "type": "number",
140
- "key": "blur",
141
- "title": "Blur",
142
- "description": "Gaussian blur radius to reduce noise",
143
- "store": False,
144
- "defaultValue": default_blur,
145
- "minimum": 1,
146
- "maximum": 21,
147
- "step": 1,
148
- "required": True,
149
- },
150
- "dilation": {
151
- "type": "number",
152
- "key": "dilation",
153
- "title": "Dilation",
154
- "description": "Expansion of detected motion areas",
155
- "store": False,
156
- "defaultValue": default_dilt,
157
- "minimum": 1,
158
- "maximum": 21,
159
- "step": 1,
160
- "required": True,
161
- },
162
- },
163
- },
164
- "background_substraction": {
165
- "type": "object",
166
- "key": "background_substraction",
167
- "title": "Background Substraction",
168
- "description": "Settings for the background substraction model",
169
- "group": "Background Substraction",
170
- "opened": True,
171
- "hidden": True,
172
- "properties": {
173
- "area": {
174
- "type": "number",
175
- "key": "area",
176
- "title": "Area",
177
- "description": "Minimum size of detected motion (pixels)",
178
- "store": False,
179
- "defaultValue": default_area_bs,
180
- "minimum": 10,
181
- "maximum": 1000,
182
- "step": 1,
183
- "required": True,
184
- },
185
- "threshold": {
186
- "type": "number",
187
- "key": "threshold",
188
- "title": "Threshold",
189
- "description": "Sensitivity of motion detection (higher = less sensitive)",
190
- "store": False,
191
- "defaultValue": default_threshold_bs,
192
- "minimum": 1,
193
- "maximum": 255,
194
- "step": 1,
195
- "required": True,
196
- },
197
- "learning_rate": {
198
- "type": "number",
199
- "key": "threshold",
200
- "title": "Learning Rate",
201
- "description": "Speed of background model adaptation (0-1, -1 for auto)",
202
- "store": False,
203
- "defaultValue": default_learning_rate,
204
- "minimum": -1,
205
- "maximum": 1,
206
- "step": 0.01,
207
- "required": True,
208
- },
209
- },
210
- },
211
- "frame_difference": {
212
- "type": "object",
213
- "key": "frame_difference",
214
- "title": "Frame Difference",
215
- "description": "Settings for the frame difference model",
216
- "group": "Frame Difference",
217
- "opened": True,
218
- "hidden": True,
219
- "properties": {
220
- "area": {
221
- "type": "number",
222
- "key": "area",
223
- "title": "Area",
224
- "description": "Minimum size of detected motion (pixels)",
225
- "store": False,
226
- "defaultValue": default_area_fd,
227
- "minimum": 10,
228
- "maximum": 1000,
229
- "step": 1,
230
- "required": True,
231
- }
232
- },
233
- },
234
- }
235
- }
236
-
237
- return root_schema
238
-
239
- async def detectMotion(self, video_path: str, config: dict[str, Any]) -> MotionDetectionPluginResponse:
240
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
241
- output_file = temp_file.name
242
-
243
- cap = cv2.VideoCapture(video_path)
244
- executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
245
-
246
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
247
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
248
- fps = int(cap.get(cv2.CAP_PROP_FPS))
249
-
250
- fourcc = cv2.VideoWriter.fourcc("a", "v", "c", "1")
251
- out = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
252
-
253
- previous_frame: Optional[np.ndarray[Any, Any]] = None
254
- backSub = None
255
-
256
- detector_model = config.get("motion_detector", "Default")
257
-
258
- while cap.isOpened():
259
- ret, frame = cap.read()
260
- if not ret:
261
- break
262
-
263
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
264
-
265
- if detector_model == "Frame Difference":
266
- area = config.get("frame_difference", {}).get("area", default_area_fd)
267
-
268
- if previous_frame is None:
269
- previous_frame = gray
270
- continue
271
-
272
- dets = await asyncio.get_event_loop().run_in_executor(
273
- executor,
274
- get_detections_fd,
275
- previous_frame,
276
- gray,
277
- area,
278
- )
279
-
280
- previous_frame = gray
281
-
282
- elif detector_model == "Background Substraction":
283
- if backSub is None:
284
- backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
285
-
286
- threshold = config.get("background_substraction", {}).get("threshold", default_threshold_bs)
287
- area = config.get("background_substraction", {}).get("area", default_area_bs)
288
-
289
- dets = await asyncio.get_event_loop().run_in_executor(
290
- executor,
291
- get_detections_bs,
292
- gray,
293
- backSub,
294
- threshold,
295
- area,
296
- )
297
-
298
- else: # default
299
- blur = config.get("default", {}).get("blur", default_blur)
300
- threshold = config.get("default", {}).get("threshold", default_threshold)
301
- area = config.get("default", {}).get("area", default_area)
302
- gray = cv2.stackBlur(gray, (blur, blur))
303
-
304
- if previous_frame is None:
305
- previous_frame = gray
306
- continue
307
-
308
- dets = await asyncio.get_event_loop().run_in_executor(
309
- executor,
310
- get_detections,
311
- previous_frame,
312
- gray,
313
- threshold,
314
- area,
315
- )
316
-
317
- previous_frame = gray
318
-
319
- merged_boxes = merge_overlapping_boxes(dets)
320
-
321
- for det in merged_boxes:
322
- (x1, y1, x2, y2) = det
323
-
324
- pt1: cv2.typing.Point = (int(x1), int(y1))
325
- pt2: cv2.typing.Point = (int(x2), int(y2))
326
-
327
- cv2.rectangle(frame, pt1, pt2, (0, 255, 0), 2)
328
-
329
- out.write(frame)
330
-
331
- cap.release()
332
- out.release()
333
- executor.shutdown()
334
-
335
- return {
336
- "filePath": output_file,
337
- }
338
-
339
-
340
- def __main__():
341
- return OpenCV
@@ -1,136 +0,0 @@
1
- from typing import Any, Union, cast
2
-
3
- import cv2
4
- import numpy as np
5
-
6
- DEFAULT_AREA_THRESHOLD = 500
7
- DEFAULT_THRESHOLD = 50
8
- DEFAULT_MASK_KERNEL = np.array((9, 9), dtype=np.uint8)
9
- DEFAULT_LEARNING_RATE = 0.08
10
-
11
-
12
- def merge_overlapping_boxes(
13
- boxes: list[tuple[float, float, float, float]], overlap_threshold: float = 0.1
14
- ) -> list[tuple[float, float, float, float]]:
15
- if not boxes:
16
- return []
17
-
18
- boxes_array = np.array(boxes)
19
- merged_boxes: list[tuple[float, float, float, float]] = []
20
-
21
- while len(boxes_array) > 0:
22
- box = boxes_array[0]
23
- boxes_array: np.ndarray[Any, Any] = boxes_array[1:]
24
-
25
- x1, y1, w1, h1 = box
26
- x2 = x1 + w1
27
- y2 = y1 + h1
28
-
29
- indices_to_remove: list[int] = []
30
- for i, (x1o, y1o, w1o, h1o) in enumerate(boxes_array):
31
- x2o = x1o + w1o
32
- y2o = y1o + h1o
33
-
34
- xx1 = max(x1, x1o)
35
- yy1 = max(y1, y1o)
36
- xx2 = min(x2, x2o)
37
- yy2 = min(y2, y2o)
38
-
39
- w_overlap = max(0, xx2 - xx1)
40
- h_overlap = max(0, yy2 - yy1)
41
- overlap_area = w_overlap * h_overlap
42
- area1 = w1 * h1
43
- area2 = w1o * h1o
44
-
45
- if area1 == 0 or area2 == 0:
46
- continue
47
- overlap_ratio = overlap_area / min(area1, area2)
48
-
49
- if overlap_ratio > overlap_threshold:
50
- x1 = min(x1, x1o)
51
- y1 = min(y1, y1o)
52
- x2 = max(x2, x2o)
53
- y2 = max(y2, y2o)
54
- indices_to_remove.append(i)
55
-
56
- boxes_array = np.delete(boxes_array, indices_to_remove, axis=0)
57
- merged_boxes.append((int(x1), int(y1), int(x2 - x1), int(y2 - y1)))
58
-
59
- return merged_boxes
60
-
61
-
62
- def get_mask(
63
- frame1: np.ndarray[Any, Any],
64
- frame2: np.ndarray[Any, Any],
65
- kernel: np.ndarray[Any, Any] = DEFAULT_MASK_KERNEL,
66
- ) -> cv2.typing.MatLike:
67
- frame_diff = cv2.subtract(frame2, frame1)
68
- frame_diff = cv2.medianBlur(frame_diff, 3)
69
-
70
- mask = cv2.adaptiveThreshold(
71
- frame_diff, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 3
72
- )
73
- mask = cv2.medianBlur(mask, 3)
74
- mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)
75
-
76
- return mask
77
-
78
-
79
- def get_motion_mask(
80
- fg_mask: cv2.typing.MatLike,
81
- threshold: int = DEFAULT_THRESHOLD,
82
- ) -> cv2.typing.MatLike:
83
- thresh = cv2.threshold(fg_mask, threshold, 255, cv2.THRESH_BINARY)[1]
84
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
85
-
86
- motion_mask = cv2.medianBlur(thresh, 3)
87
- motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_OPEN, kernel, iterations=1)
88
- motion_mask = cv2.morphologyEx(motion_mask, cv2.MORPH_CLOSE, kernel, iterations=1)
89
-
90
- return motion_mask
91
-
92
-
93
- def get_contour_detections(
94
- mask: cv2.typing.MatLike, area_threshold: int = DEFAULT_AREA_THRESHOLD
95
- ) -> list[tuple[float, float, float, float]]:
96
- cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
97
- detections: list[tuple[float, float, float, float]] = []
98
- for cnt in cnts:
99
- x, y, w, h = cv2.boundingRect(cnt)
100
- area = w * h
101
- if area > area_threshold:
102
- detections.append((x, y, x + w, y + h))
103
- return detections
104
-
105
-
106
- def get_detections(
107
- blurred_frame1: cv2.typing.MatLike,
108
- blurred_frame2: cv2.typing.MatLike,
109
- threshold: int = DEFAULT_THRESHOLD,
110
- area_threshold: int = DEFAULT_AREA_THRESHOLD,
111
- ) -> list[tuple[float, float, float, float]]:
112
- delta_frame = cv2.absdiff(blurred_frame1, blurred_frame2)
113
- thresh_frame = cv2.threshold(delta_frame, threshold, 255, cv2.THRESH_BINARY)[1]
114
- thresh_frame = cast(cv2.typing.MatLike, cv2.dilate(thresh_frame, None, iterations=2)) # type: ignore
115
- return get_contour_detections(thresh_frame, area_threshold)
116
-
117
-
118
- def get_detections_fd(
119
- frame1: np.ndarray[Any, Any],
120
- frame2: np.ndarray[Any, Any],
121
- area_threshold: int = DEFAULT_AREA_THRESHOLD,
122
- ) -> list[tuple[float, float, float, float]]:
123
- mask = get_mask(frame1, frame2, DEFAULT_MASK_KERNEL)
124
- return get_contour_detections(mask, area_threshold)
125
-
126
-
127
- def get_detections_bs(
128
- frame: np.ndarray[Any, Any],
129
- backSub: Union[cv2.BackgroundSubtractorMOG2, cv2.BackgroundSubtractorKNN],
130
- threshold: int = DEFAULT_THRESHOLD,
131
- area_threshold: int = DEFAULT_AREA_THRESHOLD,
132
- learning_rate: float = DEFAULT_LEARNING_RATE,
133
- ) -> list[tuple[float, float, float, float]]:
134
- fg_mask = backSub.apply(frame, learningRate=learning_rate)
135
- motion_mask = get_motion_mask(fg_mask, threshold=threshold)
136
- return get_contour_detections(motion_mask, area_threshold)
@@ -1,29 +0,0 @@
1
- from typing import Literal, TypedDict
2
-
3
- Detectors = Literal["Frame Difference", "Background Substraction", "Default"]
4
-
5
-
6
- class DefaultModelValues(TypedDict):
7
- area: int
8
- threshold: int
9
- blur: int
10
- dilation: int
11
- # reference_frame_frequency: int
12
-
13
-
14
- class BackgroundSubstractionValues(TypedDict):
15
- area: int
16
- threshold: int
17
- learning_rate: float
18
-
19
-
20
- class FrameDifferenceValues(TypedDict):
21
- area: int
22
- # reference_frame_frequency: int
23
-
24
-
25
- class CameraStorageValues(TypedDict):
26
- motion_detector: Detectors
27
- default: DefaultModelValues
28
- background_substraction: BackgroundSubstractionValues
29
- frame_difference: FrameDifferenceValues