@camera.ui/camera-ui-opencv 0.0.8 → 0.0.10

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "OpenCV",
3
3
  "name": "@camera.ui/camera-ui-opencv",
4
- "version": "0.0.8",
4
+ "version": "0.0.10",
5
5
  "description": "camera.ui opencv 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.1",
24
+ "camera.ui": ">=0.0.17",
25
25
  "node": ">=18.12.0"
26
26
  },
27
27
  "homepage": "https://github.com/seydx/camera.ui#readme",
package/requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
1
  numpy==1.26.4
2
2
  opencv-python-headless==4.10.0.84
3
- camera-ui-python-types==0.1.81
3
+ camera-ui-python-types==0.1.82
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import time
3
2
  from concurrent.futures import ThreadPoolExecutor
4
3
  from typing import Any, Literal, Optional
5
4
 
@@ -14,14 +13,15 @@ from camera_ui_python_types import (
14
13
  )
15
14
  from detector_defaults import (
16
15
  available_models,
17
- defaultArea,
18
- defaultAreaBs,
19
- defaultAreaFd,
20
- defaultBlur,
21
- defaultDilt,
22
- defaultReferenceFrameFrequency,
23
- defaultThreshold,
24
- defaultThresholdBs,
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
25
  )
26
26
  from opencv_utils import get_detections, get_detections_bs, get_detections_fd
27
27
  from plugin_typings import CameraStorageValues
@@ -41,6 +41,7 @@ class CameraDetector:
41
41
  self.closed = False
42
42
  self.frame_generation_task: Optional[asyncio.Task[Any]] = None
43
43
  self.executor = None
44
+ self.previous_frame: Optional[np.ndarray[Any, Any]] = None
44
45
 
45
46
  self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
46
47
  self.camera.on_connected.subscribe(lambda connected: self.start() if connected else self.close())
@@ -91,314 +92,282 @@ class CameraDetector:
91
92
  self.start()
92
93
 
93
94
  async def __detect(self) -> None:
94
- try:
95
- first_frame = None
96
- last_frame_time = 0
97
- backSub: Optional[cv2.BackgroundSubtractorMOG2] = None
98
-
99
- async for frame in self.camera.get_frames():
100
- if self.closed:
101
- break
102
-
103
- frame_image = await frame.to_image({"format": {"to": "gray"}})
104
- image_array = np.array(frame_image["image"])
105
-
106
- now = time.time() * 1000
107
-
108
- detector_model = self.__detector_model()
109
-
110
- if detector_model == "Frame Difference":
111
- area = self.camera_storage.values["frame_difference"]["area"]
112
- reference_frame_frequency = self.camera_storage.values["frame_difference"][
113
- "reference_frame_frequency"
114
- ]
115
-
116
- if first_frame is None or (
117
- reference_frame_frequency
118
- and now - last_frame_time > (reference_frame_frequency * 1000)
119
- ):
120
- first_frame = image_array
121
- last_frame_time = now
122
- continue
123
-
124
- dets = await asyncio.get_event_loop().run_in_executor(
125
- self.executor,
126
- get_detections_fd,
127
- first_frame,
128
- image_array,
129
- area,
130
- )
131
-
132
- elif detector_model == "Background Substraction":
133
- if backSub is None:
134
- backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
135
-
136
- threshold = self.camera_storage.values["background_substraction"]["threshold"]
137
- area = self.camera_storage.values["background_substraction"]["area"]
138
-
139
- dets = await asyncio.get_event_loop().run_in_executor(
140
- self.executor,
141
- get_detections_bs,
142
- image_array,
143
- backSub,
144
- threshold,
145
- area,
146
- )
147
-
148
- else: # default
149
- blur = self.camera_storage.values["default"]["blur"]
150
- threshold = self.camera_storage.values["default"]["threshold"]
151
- area = self.camera_storage.values["default"]["area"]
152
- reference_frame_frequency = self.camera_storage.values["default"][
153
- "reference_frame_frequency"
154
- ]
155
-
156
- image_array = cv2.stackBlur(image_array, (blur, blur))
157
-
158
- if first_frame is None or (
159
- reference_frame_frequency
160
- and now - last_frame_time > (reference_frame_frequency * 1000)
161
- ):
162
- first_frame = image_array
163
- last_frame_time = now
164
- continue
165
-
166
- dets = await asyncio.get_event_loop().run_in_executor(
167
- self.executor,
168
- get_detections,
169
- first_frame,
170
- image_array,
171
- threshold,
172
- area,
173
- )
174
-
175
- detections: list[Detection] = []
176
-
177
- for det in dets:
178
- (x1, y1, x2, y2) = det
179
-
180
- detection: Detection = {
181
- "label": "motion",
182
- "confidence": 1,
183
- "boundingBox": (x1, y1, x2, y2),
184
- "inputWidth": frame_image["info"]["width"],
185
- "inputHeight": frame_image["info"]["height"],
186
- "origWidth": frame.metadata["origWidth"],
187
- "origHeight": frame.metadata["origHeight"],
188
- }
189
-
190
- detections.append(detection)
191
-
192
- await self.camera.update_state("motion", {"detections": detections}, frame)
193
-
194
- except asyncio.CancelledError:
195
- pass
196
- except Exception as error:
197
- self.logger.error(self.camera.name, "Error generating frames", error)
198
- finally:
199
- if not self.closed:
200
- self.restart()
95
+ while not self.closed:
96
+ try:
97
+ backSub: Optional[cv2.BackgroundSubtractorMOG2] = None
98
+
99
+ async for frame in self.camera.get_frames():
100
+ if self.closed:
101
+ break
102
+
103
+ frame_image = await frame.to_image({"format": {"to": "gray"}})
104
+ image_array = np.array(frame_image["image"])
105
+
106
+ detector_model = self.__detector_model()
107
+
108
+ if detector_model == "Frame Difference":
109
+ area = self.camera_storage.values["frame_difference"]["area"]
110
+
111
+ if self.previous_frame is None:
112
+ self.previous_frame = image_array
113
+ continue
114
+
115
+ dets = await asyncio.get_event_loop().run_in_executor(
116
+ self.executor,
117
+ get_detections_fd,
118
+ self.previous_frame,
119
+ image_array,
120
+ area,
121
+ )
122
+
123
+ self.previous_frame = image_array
124
+
125
+ elif detector_model == "Background Substraction":
126
+ if backSub is None:
127
+ backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
128
+
129
+ threshold = self.camera_storage.values["background_substraction"]["threshold"]
130
+ area = self.camera_storage.values["background_substraction"]["area"]
131
+ learning_rate = self.camera_storage.values["background_substraction"]["learning_rate"]
132
+
133
+ dets = await asyncio.get_event_loop().run_in_executor(
134
+ self.executor,
135
+ get_detections_bs,
136
+ image_array,
137
+ backSub,
138
+ threshold,
139
+ area,
140
+ learning_rate,
141
+ )
142
+
143
+ else: # default
144
+ blur = self.camera_storage.values["default"]["blur"]
145
+ threshold = self.camera_storage.values["default"]["threshold"]
146
+ area = self.camera_storage.values["default"]["area"]
147
+ image_array = cv2.stackBlur(image_array, (blur, blur))
148
+
149
+ if self.previous_frame is None:
150
+ self.previous_frame = image_array
151
+ continue
152
+
153
+ dets = await asyncio.get_event_loop().run_in_executor(
154
+ self.executor,
155
+ get_detections,
156
+ self.previous_frame,
157
+ image_array,
158
+ threshold,
159
+ area,
160
+ )
161
+
162
+ self.previous_frame = image_array
163
+
164
+ detections: list[Detection] = []
165
+
166
+ for det in dets:
167
+ (x1, y1, x2, y2) = det
168
+
169
+ detection: Detection = {
170
+ "label": "motion",
171
+ "confidence": 1,
172
+ "boundingBox": (x1, y1, x2, y2),
173
+ "inputWidth": frame_image["info"]["width"],
174
+ "inputHeight": frame_image["info"]["height"],
175
+ "origWidth": frame.metadata["origWidth"],
176
+ "origHeight": frame.metadata["origHeight"],
177
+ }
178
+
179
+ detections.append(detection)
180
+
181
+ await self.camera.update_state("motion", {"detections": detections}, frame)
182
+ except asyncio.CancelledError:
183
+ break
184
+ except Exception as error:
185
+ self.logger.error(self.camera.name, "Error generating frames", error)
186
+ self.logger.log(self.camera.name, "Restarting motion detection in 5 seconds...")
187
+ await asyncio.sleep(5)
188
+ continue
201
189
 
202
190
  def __detector_model(self) -> Literal["Frame Difference", "Background Substraction", "Default"]:
203
191
  return self.camera_storage.values["motion_detector"]
204
192
 
205
193
  def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
206
- camera_storage: Optional[CameraStorage[CameraStorageValues]] = (
207
- self.api.storage_controller.get_camera_storage(self.camera.id)
208
- )
209
-
210
- if camera_storage is not None:
211
- return camera_storage
212
-
213
- camera_storage = self.api.storage_controller.create_camera_storage(
214
- self,
215
- self.camera.id,
216
- {
217
- "motion_detector": {
218
- "type": "string",
219
- "key": "motion_detector",
220
- "title": "Motion Detector",
221
- "description": "Select the motion detection model to use",
222
- "group": "Manage",
223
- "enum": available_models,
224
- "store": True,
225
- "defaultValue": "Default",
226
- "required": True,
227
- "onSet": self.__change_model,
228
- },
229
- "default": {
230
- "type": "object",
231
- "key": "default",
232
- "title": "Default",
233
- "description": "Settings for the default model",
234
- "group": "Default",
235
- "opened": True,
236
- "hidden": True,
237
- "properties": {
238
- "area": {
239
- "type": "number",
240
- "key": "area",
241
- "title": "Area",
242
- "description": "Minimum area for a motion detection",
243
- "store": True,
244
- "defaultValue": defaultArea,
245
- "minimum": 10,
246
- "maximum": 1000,
247
- "step": 1,
248
- "required": True,
249
- "onSet": lambda old, new: self.logger.log(
250
- self.camera.name, f"Default model motion area changed from {old} to {new}"
251
- ),
252
- },
253
- "threshold": {
254
- "type": "number",
255
- "key": "threshold",
256
- "title": "Threshold",
257
- "description": "Threshold for the default model",
258
- "store": True,
259
- "defaultValue": defaultThreshold,
260
- "minimum": 1,
261
- "maximum": 255,
262
- "step": 1,
263
- "required": True,
264
- "onSet": lambda old, new: self.logger.log(
265
- self.camera.name, f"Default model threshold changed from {old} to {new}"
266
- ),
267
- },
268
- "blur": {
269
- "type": "number",
270
- "key": "blur",
271
- "title": "Blur",
272
- "description": "Blur for the default model",
273
- "store": True,
274
- "defaultValue": defaultBlur,
275
- "minimum": 1,
276
- "maximum": 21,
277
- "step": 1,
278
- "required": True,
279
- "onSet": lambda old, new: self.logger.log(
280
- self.camera.name, f"Default model blur radius changed from {old} to {new}"
281
- ),
282
- },
283
- "dilation": {
284
- "type": "number",
285
- "key": "dilation",
286
- "title": "Dilation",
287
- "description": "Dilation for the default model",
288
- "store": True,
289
- "defaultValue": defaultDilt,
290
- "minimum": 1,
291
- "maximum": 21,
292
- "step": 1,
293
- "required": True,
294
- "onSet": lambda old, new: self.logger.log(
295
- self.camera.name, f"Default model dilation size changed from {old} to {new}"
296
- ),
297
- },
298
- "reference_frame_frequency": {
299
- "type": "number",
300
- "key": "reference_frame_frequency",
301
- "title": "Reference Frame Frequency",
302
- "description": "Frequency to update the reference frame",
303
- "store": True,
304
- "defaultValue": defaultReferenceFrameFrequency,
305
- "minimum": 1,
306
- "maximum": 60,
307
- "step": 1,
308
- "required": True,
309
- "onSet": lambda old, new: self.logger.log(
310
- self.camera.name,
311
- f"Default model reference frame frequency changed from {old} to {new}",
312
- ),
313
- },
194
+ camera_storage: CameraStorage[CameraStorageValues] = (
195
+ self.api.storage_controller.create_camera_storage(
196
+ self,
197
+ self.camera.id,
198
+ {
199
+ "motion_detector": {
200
+ "type": "string",
201
+ "key": "motion_detector",
202
+ "title": "Motion Detector",
203
+ "description": "Select the motion detection model to use",
204
+ "group": "Manage",
205
+ "enum": available_models,
206
+ "store": True,
207
+ "defaultValue": default_model,
208
+ "required": True,
209
+ "onSet": self.__change_model,
314
210
  },
315
- },
316
- "background_substraction": {
317
- "type": "object",
318
- "key": "background_substraction",
319
- "title": "Background Substraction",
320
- "description": "Settings for the background substraction model",
321
- "group": "Background Substraction",
322
- "opened": True,
323
- "hidden": True,
324
- "properties": {
325
- "area": {
326
- "type": "number",
327
- "key": "area",
328
- "title": "Area",
329
- "description": "Minimum area for a motion detection",
330
- "store": True,
331
- "defaultValue": defaultAreaBs,
332
- "minimum": 10,
333
- "maximum": 1000,
334
- "step": 1,
335
- "required": True,
336
- "onSet": lambda old, new: self.logger.log(
337
- self.camera.name,
338
- f"Background substraction model area changed from {old} to {new}",
339
- ),
340
- },
341
- "threshold": {
342
- "type": "number",
343
- "key": "threshold",
344
- "title": "Threshold",
345
- "description": "Threshold for the background substraction",
346
- "store": True,
347
- "defaultValue": defaultThresholdBs,
348
- "minimum": 1,
349
- "maximum": 255,
350
- "step": 1,
351
- "required": True,
352
- "onSet": lambda old, new: self.logger.log(
353
- self.camera.name,
354
- f"Background substraction model threshold changed from {old} to {new}",
355
- ),
211
+ "default": {
212
+ "type": "object",
213
+ "key": "default",
214
+ "title": "Default",
215
+ "description": "Settings for the default model",
216
+ "group": "Default",
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": True,
226
+ "defaultValue": default_area,
227
+ "minimum": 10,
228
+ "maximum": 1000,
229
+ "step": 1,
230
+ "required": True,
231
+ "onSet": lambda old, new: self.logger.log(
232
+ self.camera.name, f"Default model motion area changed from {old} to {new}"
233
+ ),
234
+ },
235
+ "threshold": {
236
+ "type": "number",
237
+ "key": "threshold",
238
+ "title": "Threshold",
239
+ "description": "Sensitivity of motion detection (higher = less sensitive)",
240
+ "store": True,
241
+ "defaultValue": default_threshold,
242
+ "minimum": 1,
243
+ "maximum": 255,
244
+ "step": 1,
245
+ "required": True,
246
+ "onSet": lambda old, new: self.logger.log(
247
+ self.camera.name, f"Default model threshold changed from {old} to {new}"
248
+ ),
249
+ },
250
+ "blur": {
251
+ "type": "number",
252
+ "key": "blur",
253
+ "title": "Blur",
254
+ "description": "Gaussian blur radius to reduce noise",
255
+ "store": True,
256
+ "defaultValue": default_blur,
257
+ "minimum": 1,
258
+ "maximum": 21,
259
+ "step": 1,
260
+ "required": True,
261
+ "onSet": lambda old, new: self.logger.log(
262
+ self.camera.name, f"Default model blur radius changed from {old} to {new}"
263
+ ),
264
+ },
265
+ "dilation": {
266
+ "type": "number",
267
+ "key": "dilation",
268
+ "title": "Dilation",
269
+ "description": "Expansion of detected motion areas",
270
+ "store": True,
271
+ "defaultValue": default_dilt,
272
+ "minimum": 1,
273
+ "maximum": 21,
274
+ "step": 1,
275
+ "required": True,
276
+ "onSet": lambda old, new: self.logger.log(
277
+ self.camera.name,
278
+ f"Default model dilation size changed from {old} to {new}",
279
+ ),
280
+ },
356
281
  },
357
282
  },
358
- },
359
- "frame_difference": {
360
- "type": "object",
361
- "key": "frame_difference",
362
- "title": "Frame Difference",
363
- "description": "Settings for the frame difference model",
364
- "group": "Frame Difference",
365
- "opened": True,
366
- "hidden": True,
367
- "properties": {
368
- "area": {
369
- "type": "number",
370
- "key": "area",
371
- "title": "Area",
372
- "description": "Minimum area for a motion detection",
373
- "store": True,
374
- "defaultValue": defaultAreaFd,
375
- "minimum": 10,
376
- "maximum": 1000,
377
- "step": 1,
378
- "required": True,
379
- "onSet": lambda old, new: self.logger.log(
380
- self.camera.name, f"Frame difference model area changed from {old} to {new}"
381
- ),
283
+ "background_substraction": {
284
+ "type": "object",
285
+ "key": "background_substraction",
286
+ "title": "Background Substraction",
287
+ "description": "Settings for the background substraction model",
288
+ "group": "Background Substraction",
289
+ "opened": True,
290
+ "hidden": True,
291
+ "properties": {
292
+ "area": {
293
+ "type": "number",
294
+ "key": "area",
295
+ "title": "Area",
296
+ "description": "Minimum size of detected motion (pixels)",
297
+ "store": True,
298
+ "defaultValue": default_area_bs,
299
+ "minimum": 10,
300
+ "maximum": 1000,
301
+ "step": 1,
302
+ "required": True,
303
+ "onSet": lambda old, new: self.logger.log(
304
+ self.camera.name,
305
+ f"Background substraction model area changed from {old} to {new}",
306
+ ),
307
+ },
308
+ "threshold": {
309
+ "type": "number",
310
+ "key": "threshold",
311
+ "title": "Threshold",
312
+ "description": "Sensitivity of motion detection (higher = less sensitive)",
313
+ "store": True,
314
+ "defaultValue": default_threshold_bs,
315
+ "minimum": 1,
316
+ "maximum": 255,
317
+ "step": 1,
318
+ "required": True,
319
+ "onSet": lambda old, new: self.logger.log(
320
+ self.camera.name,
321
+ f"Background substraction model threshold changed from {old} to {new}",
322
+ ),
323
+ },
324
+ "learning_rate": {
325
+ "type": "number",
326
+ "key": "threshold",
327
+ "title": "Learning Rate",
328
+ "description": "Speed of background model adaptation (0-1, -1 for auto)",
329
+ "store": True,
330
+ "defaultValue": default_learning_rate,
331
+ "minimum": -1,
332
+ "maximum": 1,
333
+ "step": 0.01,
334
+ "required": True,
335
+ "onSet": lambda old, new: self.logger.log(
336
+ self.camera.name,
337
+ f"Background substraction model threshold changed from {old} to {new}",
338
+ ),
339
+ },
382
340
  },
383
- "reference_frame_frequency": {
384
- "type": "number",
385
- "key": "reference_frame_frequency",
386
- "title": "Reference Frame Frequency",
387
- "description": "Frequency to update the reference frame",
388
- "store": True,
389
- "defaultValue": defaultReferenceFrameFrequency,
390
- "minimum": 1,
391
- "maximum": 60,
392
- "step": 1,
393
- "required": True,
394
- "onSet": lambda old, new: self.logger.log(
395
- self.camera.name,
396
- f"Frae difference model reference frame frequency changed from {old} to {new}",
397
- ),
341
+ },
342
+ "frame_difference": {
343
+ "type": "object",
344
+ "key": "frame_difference",
345
+ "title": "Frame Difference",
346
+ "description": "Settings for the frame difference model",
347
+ "group": "Frame Difference",
348
+ "opened": True,
349
+ "hidden": True,
350
+ "properties": {
351
+ "area": {
352
+ "type": "number",
353
+ "key": "area",
354
+ "title": "Area",
355
+ "description": "Minimum size of detected motion (pixels)",
356
+ "store": True,
357
+ "defaultValue": default_area_fd,
358
+ "minimum": 10,
359
+ "maximum": 1000,
360
+ "step": 1,
361
+ "required": True,
362
+ "onSet": lambda old, new: self.logger.log(
363
+ self.camera.name,
364
+ f"Frame difference model area changed from {old} to {new}",
365
+ ),
366
+ }
398
367
  },
399
368
  },
400
369
  },
401
- },
370
+ )
402
371
  )
403
372
 
404
373
  current_model = camera_storage.values["motion_detector"]
@@ -421,6 +390,7 @@ class CameraDetector:
421
390
 
422
391
  old_schema = None
423
392
  new_schema = None
393
+ self.previous_frame = None
424
394
 
425
395
  if old_model == "Default":
426
396
  old_schema = self.camera_storage.getSchema("default")
@@ -1,13 +1,16 @@
1
- defaultArea = 250
2
- defaultAreaFd = 500
3
- defaultAreaBs = 500
1
+ default_area = 350
2
+ default_area_fd = 350
3
+ default_area_bs = 350
4
4
 
5
- defaultThreshold = 50
6
- defaultThresholdBs = 200
5
+ default_threshold = 50
6
+ default_threshold_bs = 50
7
7
 
8
- defaultBlur = 9
9
- defaultDilt = 3
8
+ default_blur = 11
9
+ default_dilt = 15
10
10
 
11
- defaultReferenceFrameFrequency = 5
11
+ default_learning_rate = -1
12
12
 
13
13
  available_models = ["Frame Difference", "Background Substraction", "Default"]
14
+
15
+
16
+ default_model = "Background Substraction"
package/src/main.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import asyncio
2
2
  import tempfile
3
- import time
4
3
  from concurrent.futures import ThreadPoolExecutor
5
- from typing import Any, Union
4
+ from typing import Any, Optional, Union
6
5
 
7
6
  import cv2
7
+ import numpy as np
8
8
  from camera_detector import CameraDetector
9
9
  from camera_ui_python_types import (
10
10
  CameraDevice,
@@ -17,13 +17,15 @@ from camera_ui_python_types import (
17
17
  RootSchema,
18
18
  )
19
19
  from detector_defaults import (
20
- defaultArea,
21
- defaultAreaBs,
22
- defaultAreaFd,
23
- defaultBlur,
24
- defaultReferenceFrameFrequency,
25
- defaultThreshold,
26
- defaultThresholdBs,
20
+ default_area,
21
+ default_area_bs,
22
+ default_area_fd,
23
+ default_blur,
24
+ default_dilt,
25
+ default_learning_rate,
26
+ default_model,
27
+ default_threshold,
28
+ default_threshold_bs,
27
29
  )
28
30
  from opencv_utils import get_detections, get_detections_bs, get_detections_fd
29
31
 
@@ -96,7 +98,7 @@ class OpenCV(MotionDetectionPlugin):
96
98
  "description": "Select the motion detection model to use",
97
99
  "enum": ["Frame Difference", "Background Substraction", "Default"],
98
100
  "store": False,
99
- "defaultValue": "Default",
101
+ "defaultValue": default_model,
100
102
  "required": True,
101
103
  },
102
104
  "default": {
@@ -104,15 +106,17 @@ class OpenCV(MotionDetectionPlugin):
104
106
  "key": "default",
105
107
  "title": "Default",
106
108
  "description": "Settings for the default model",
107
- "opened": False,
109
+ "group": "Default",
110
+ "opened": True,
111
+ "hidden": True,
108
112
  "properties": {
109
113
  "area": {
110
114
  "type": "number",
111
115
  "key": "area",
112
116
  "title": "Area",
113
- "description": "Minimum area for a motion detection",
117
+ "description": "Minimum size of detected motion (pixels)",
114
118
  "store": False,
115
- "defaultValue": 250,
119
+ "defaultValue": default_area,
116
120
  "minimum": 10,
117
121
  "maximum": 1000,
118
122
  "step": 1,
@@ -122,9 +126,9 @@ class OpenCV(MotionDetectionPlugin):
122
126
  "type": "number",
123
127
  "key": "threshold",
124
128
  "title": "Threshold",
125
- "description": "Threshold for the default model",
129
+ "description": "Sensitivity of motion detection (higher = less sensitive)",
126
130
  "store": False,
127
- "defaultValue": 50,
131
+ "defaultValue": default_threshold,
128
132
  "minimum": 1,
129
133
  "maximum": 255,
130
134
  "step": 1,
@@ -134,9 +138,9 @@ class OpenCV(MotionDetectionPlugin):
134
138
  "type": "number",
135
139
  "key": "blur",
136
140
  "title": "Blur",
137
- "description": "Blur for the default model",
141
+ "description": "Gaussian blur radius to reduce noise",
138
142
  "store": False,
139
- "defaultValue": 9,
143
+ "defaultValue": default_blur,
140
144
  "minimum": 1,
141
145
  "maximum": 21,
142
146
  "step": 1,
@@ -146,26 +150,14 @@ class OpenCV(MotionDetectionPlugin):
146
150
  "type": "number",
147
151
  "key": "dilation",
148
152
  "title": "Dilation",
149
- "description": "Dilation for the default model",
153
+ "description": "Expansion of detected motion areas",
150
154
  "store": False,
151
- "defaultValue": 3,
155
+ "defaultValue": default_dilt,
152
156
  "minimum": 1,
153
157
  "maximum": 21,
154
158
  "step": 1,
155
159
  "required": True,
156
160
  },
157
- "reference_frame_frequency": {
158
- "type": "number",
159
- "key": "reference_frame_frequency",
160
- "title": "Reference Frame Frequency",
161
- "description": "Frequency to update the reference frame",
162
- "store": False,
163
- "defaultValue": 5,
164
- "minimum": 1,
165
- "maximum": 60,
166
- "step": 1,
167
- "required": True,
168
- },
169
161
  },
170
162
  },
171
163
  "background_substraction": {
@@ -173,15 +165,17 @@ class OpenCV(MotionDetectionPlugin):
173
165
  "key": "background_substraction",
174
166
  "title": "Background Substraction",
175
167
  "description": "Settings for the background substraction model",
176
- "opened": False,
168
+ "group": "Background Substraction",
169
+ "opened": True,
170
+ "hidden": True,
177
171
  "properties": {
178
172
  "area": {
179
173
  "type": "number",
180
174
  "key": "area",
181
175
  "title": "Area",
182
- "description": "Minimum area for a motion detection",
176
+ "description": "Minimum size of detected motion (pixels)",
183
177
  "store": False,
184
- "defaultValue": 500,
178
+ "defaultValue": default_area_bs,
185
179
  "minimum": 10,
186
180
  "maximum": 1000,
187
181
  "step": 1,
@@ -191,14 +185,26 @@ class OpenCV(MotionDetectionPlugin):
191
185
  "type": "number",
192
186
  "key": "threshold",
193
187
  "title": "Threshold",
194
- "description": "Threshold for the background substraction",
188
+ "description": "Sensitivity of motion detection (higher = less sensitive)",
195
189
  "store": False,
196
- "defaultValue": 200,
190
+ "defaultValue": default_threshold_bs,
197
191
  "minimum": 1,
198
192
  "maximum": 255,
199
193
  "step": 1,
200
194
  "required": True,
201
195
  },
196
+ "learning_rate": {
197
+ "type": "number",
198
+ "key": "threshold",
199
+ "title": "Learning Rate",
200
+ "description": "Speed of background model adaptation (0-1, -1 for auto)",
201
+ "store": False,
202
+ "defaultValue": default_learning_rate,
203
+ "minimum": -1,
204
+ "maximum": 1,
205
+ "step": 0.01,
206
+ "required": True,
207
+ },
202
208
  },
203
209
  },
204
210
  "frame_difference": {
@@ -206,32 +212,22 @@ class OpenCV(MotionDetectionPlugin):
206
212
  "key": "frame_difference",
207
213
  "title": "Frame Difference",
208
214
  "description": "Settings for the frame difference model",
209
- "opened": False,
215
+ "group": "Frame Difference",
216
+ "opened": True,
217
+ "hidden": True,
210
218
  "properties": {
211
219
  "area": {
212
220
  "type": "number",
213
221
  "key": "area",
214
222
  "title": "Area",
215
- "description": "Minimum area for a motion detection",
223
+ "description": "Minimum size of detected motion (pixels)",
216
224
  "store": False,
217
- "defaultValue": 500,
225
+ "defaultValue": default_area_fd,
218
226
  "minimum": 10,
219
227
  "maximum": 1000,
220
228
  "step": 1,
221
229
  "required": True,
222
- },
223
- "reference_frame_frequency": {
224
- "type": "number",
225
- "key": "reference_frame_frequency",
226
- "title": "Reference Frame Frequency",
227
- "description": "Frequency to update the reference frame",
228
- "store": False,
229
- "defaultValue": 5,
230
- "minimum": 1,
231
- "maximum": 60,
232
- "step": 1,
233
- "required": True,
234
- },
230
+ }
235
231
  },
236
232
  },
237
233
  }
@@ -251,8 +247,7 @@ class OpenCV(MotionDetectionPlugin):
251
247
  fourcc = cv2.VideoWriter.fourcc("a", "v", "c", "1")
252
248
  out = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
253
249
 
254
- first_frame = None
255
- last_frame_time = 0
250
+ previous_frame: Optional[np.ndarray[Any, Any]] = None
256
251
  backSub = None
257
252
 
258
253
  detector_model = config.get("motion_detector", "Default")
@@ -263,35 +258,30 @@ class OpenCV(MotionDetectionPlugin):
263
258
  break
264
259
 
265
260
  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
266
- now = time.time() * 1000
267
261
 
268
262
  if detector_model == "Frame Difference":
269
- area = config.get("frame_difference", {}).get("area", defaultAreaFd)
270
- reference_frame_frequency = config.get("frame_difference", {}).get(
271
- "reference_frame_frequency", defaultReferenceFrameFrequency
272
- )
263
+ area = config.get("frame_difference", {}).get("area", default_area_fd)
273
264
 
274
- if first_frame is None or (
275
- reference_frame_frequency and now - last_frame_time > (reference_frame_frequency * 1000)
276
- ):
277
- first_frame = gray
278
- last_frame_time = now
265
+ if previous_frame is None:
266
+ previous_frame = gray
279
267
  continue
280
268
 
281
269
  dets = await asyncio.get_event_loop().run_in_executor(
282
270
  executor,
283
271
  get_detections_fd,
284
- first_frame,
272
+ previous_frame,
285
273
  gray,
286
274
  area,
287
275
  )
288
276
 
277
+ previous_frame = gray
278
+
289
279
  elif detector_model == "Background Substraction":
290
280
  if backSub is None:
291
281
  backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
292
282
 
293
- threshold = config.get("background_substraction", {}).get("threshold", defaultThresholdBs)
294
- area = config.get("background_substraction", {}).get("area", defaultAreaBs)
283
+ threshold = config.get("background_substraction", {}).get("threshold", default_threshold_bs)
284
+ area = config.get("background_substraction", {}).get("area", default_area_bs)
295
285
 
296
286
  dets = await asyncio.get_event_loop().run_in_executor(
297
287
  executor,
@@ -303,31 +293,26 @@ class OpenCV(MotionDetectionPlugin):
303
293
  )
304
294
 
305
295
  else: # default
306
- blur = config.get("default", {}).get("blur", defaultBlur)
307
- threshold = config.get("default", {}).get("threshold", defaultThreshold)
308
- area = config.get("default", {}).get("area", defaultArea)
309
- reference_frame_frequency = config.get("default", {}).get(
310
- "reference_frame_frequency", defaultReferenceFrameFrequency
311
- )
312
-
296
+ blur = config.get("default", {}).get("blur", default_blur)
297
+ threshold = config.get("default", {}).get("threshold", default_threshold)
298
+ area = config.get("default", {}).get("area", default_area)
313
299
  gray = cv2.stackBlur(gray, (blur, blur))
314
300
 
315
- if first_frame is None or (
316
- reference_frame_frequency and now - last_frame_time > (reference_frame_frequency * 1000)
317
- ):
318
- first_frame = gray
319
- last_frame_time = now
301
+ if previous_frame is None:
302
+ previous_frame = gray
320
303
  continue
321
304
 
322
305
  dets = await asyncio.get_event_loop().run_in_executor(
323
306
  executor,
324
307
  get_detections,
325
- first_frame,
308
+ previous_frame,
326
309
  gray,
327
310
  threshold,
328
311
  area,
329
312
  )
330
313
 
314
+ previous_frame = gray
315
+
331
316
  for det in dets:
332
317
  (x1, y1, x2, y2) = det
333
318
 
@@ -6,6 +6,7 @@ import numpy as np
6
6
  DEFAULT_AREA_THRESHOLD = 500
7
7
  DEFAULT_THRESHOLD = 50
8
8
  DEFAULT_MASK_KERNEL = np.array((9, 9), dtype=np.uint8)
9
+ DEFAULT_LEARNING_RATE = 0.08
9
10
 
10
11
 
11
12
  def get_mask(
@@ -52,91 +53,6 @@ def get_contour_detections(
52
53
  return detections
53
54
 
54
55
 
55
- def is_in_bbox(point: tuple[float, float], bbox: tuple[float, float, float, float]) -> bool:
56
- return point[0] >= bbox[0] and point[0] <= bbox[2] and point[1] >= bbox[1] and point[1] <= bbox[3]
57
-
58
-
59
- def intersect(bbox: tuple[float, float, float, float], bbox_: tuple[float, float, float, float]) -> bool:
60
- for i in range(int(len(bbox) / 2)):
61
- for j in range(int(len(bbox) / 2)):
62
- # Check if one of the corner of bbox inside bbox_
63
- if is_in_bbox((bbox[2 * i], bbox[2 * j + 1]), bbox_):
64
- return True
65
- return False
66
-
67
-
68
- def good_intersect(bbox: tuple[float, float, float, float], bbox_: tuple[float, float, float, float]) -> bool:
69
- return bbox[0] < bbox_[2] and bbox[2] > bbox_[0] and bbox[1] < bbox_[3] and bbox[3] > bbox_[1]
70
-
71
-
72
- def calculate_iou(
73
- bbox1: tuple[float, float, float, float], bbox2: tuple[float, float, float, float]
74
- ) -> float:
75
- x1 = max(bbox1[0], bbox2[0])
76
- y1 = max(bbox1[1], bbox2[1])
77
- x2 = min(bbox1[2], bbox2[2])
78
- y2 = min(bbox1[3], bbox2[3])
79
-
80
- intersection_area = max(0, x2 - x1) * max(0, y2 - y1)
81
-
82
- bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
83
- bbox2_area = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
84
-
85
- union_area = bbox1_area + bbox2_area - intersection_area
86
-
87
- iou = intersection_area / union_area
88
-
89
- return iou
90
-
91
-
92
- def merge_bboxes(
93
- bboxes: list[tuple[float, float, float, float]],
94
- delta_x: float = 0.4,
95
- delta_y: float = 0.4,
96
- iou_threshold: float = 0,
97
- ) -> list[tuple[float, float, float, float]]:
98
- # Sort bboxes by ymin
99
- bboxes = sorted(bboxes, key=lambda x: x[1])
100
-
101
- merged_bboxes: list[tuple[float, float, float, float]] = []
102
-
103
- for bbox in bboxes:
104
- is_merged = False
105
-
106
- for i in range(len(merged_bboxes)):
107
- merged_bbox = merged_bboxes[i]
108
-
109
- # Compute the bboxes with a margin
110
- bmargin: tuple[float, float, float, float] = (
111
- bbox[0] - (bbox[2] - bbox[0]) * delta_x,
112
- bbox[1] - (bbox[3] - bbox[1]) * delta_y,
113
- bbox[2] + (bbox[2] - bbox[0]) * delta_x,
114
- bbox[3] + (bbox[3] - bbox[1]) * delta_y,
115
- )
116
- merged_bmargin: tuple[float, float, float, float] = (
117
- merged_bbox[0] - (merged_bbox[2] - merged_bbox[0]) * delta_x,
118
- merged_bbox[1] - (merged_bbox[3] - merged_bbox[1]) * delta_y,
119
- merged_bbox[2] + (merged_bbox[2] - merged_bbox[0]) * delta_x,
120
- merged_bbox[3] + (merged_bbox[3] - merged_bbox[1]) * delta_y,
121
- )
122
-
123
- # Check if bboxes with margin have an intersection or if IoU is above the threshold
124
- if intersect(bmargin, merged_bmargin) or calculate_iou(bbox, merged_bbox) >= iou_threshold:
125
- merged_bboxes[i] = (
126
- min(bbox[0], merged_bbox[0]),
127
- min(bbox[1], merged_bbox[1]),
128
- max(bbox[2], merged_bbox[2]),
129
- max(bbox[3], merged_bbox[3]),
130
- )
131
- is_merged = True
132
- break
133
-
134
- if not is_merged:
135
- merged_bboxes.append(bbox)
136
-
137
- return merged_bboxes
138
-
139
-
140
56
  def get_detections(
141
57
  blurred_frame1: cv2.typing.MatLike,
142
58
  blurred_frame2: cv2.typing.MatLike,
@@ -146,8 +62,7 @@ def get_detections(
146
62
  delta_frame = cv2.absdiff(blurred_frame1, blurred_frame2)
147
63
  thresh_frame = cv2.threshold(delta_frame, threshold, 255, cv2.THRESH_BINARY)[1]
148
64
  thresh_frame = cast(cv2.typing.MatLike, cv2.dilate(thresh_frame, None, iterations=2)) # type: ignore
149
- detections = get_contour_detections(thresh_frame, area_threshold)
150
- return merge_bboxes(detections)
65
+ return get_contour_detections(thresh_frame, area_threshold)
151
66
 
152
67
 
153
68
  def get_detections_fd(
@@ -156,8 +71,7 @@ def get_detections_fd(
156
71
  area_threshold: int = DEFAULT_AREA_THRESHOLD,
157
72
  ) -> list[tuple[float, float, float, float]]:
158
73
  mask = get_mask(frame1, frame2, DEFAULT_MASK_KERNEL)
159
- detections = get_contour_detections(mask, area_threshold)
160
- return merge_bboxes(detections)
74
+ return get_contour_detections(mask, area_threshold)
161
75
 
162
76
 
163
77
  def get_detections_bs(
@@ -165,8 +79,8 @@ def get_detections_bs(
165
79
  backSub: Union[cv2.BackgroundSubtractorMOG2, cv2.BackgroundSubtractorKNN],
166
80
  threshold: int = DEFAULT_THRESHOLD,
167
81
  area_threshold: int = DEFAULT_AREA_THRESHOLD,
82
+ learning_rate: float = DEFAULT_LEARNING_RATE,
168
83
  ) -> list[tuple[float, float, float, float]]:
169
- fg_mask = backSub.apply(frame)
84
+ fg_mask = backSub.apply(frame, learningRate=learning_rate)
170
85
  motion_mask = get_motion_mask(fg_mask, threshold=threshold)
171
- detections = get_contour_detections(motion_mask, area_threshold)
172
- return merge_bboxes(detections)
86
+ return get_contour_detections(motion_mask, area_threshold)
@@ -8,17 +8,18 @@ class DefaultModelValues(TypedDict):
8
8
  threshold: int
9
9
  blur: int
10
10
  dilation: int
11
- reference_frame_frequency: int
11
+ # reference_frame_frequency: int
12
12
 
13
13
 
14
14
  class BackgroundSubstractionValues(TypedDict):
15
15
  area: int
16
16
  threshold: int
17
+ learning_rate: float
17
18
 
18
19
 
19
20
  class FrameDifferenceValues(TypedDict):
20
21
  area: int
21
- reference_frame_frequency: int
22
+ # reference_frame_frequency: int
22
23
 
23
24
 
24
25
  class CameraStorageValues(TypedDict):