@camera.ui/camera-ui-opencv 0.0.9 → 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.9",
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())
@@ -93,8 +94,6 @@ class CameraDetector:
93
94
  async def __detect(self) -> None:
94
95
  while not self.closed:
95
96
  try:
96
- first_frame = None
97
- last_frame_time = 0
98
97
  backSub: Optional[cv2.BackgroundSubtractorMOG2] = None
99
98
 
100
99
  async for frame in self.camera.get_frames():
@@ -104,38 +103,32 @@ class CameraDetector:
104
103
  frame_image = await frame.to_image({"format": {"to": "gray"}})
105
104
  image_array = np.array(frame_image["image"])
106
105
 
107
- now = time.time() * 1000
108
-
109
106
  detector_model = self.__detector_model()
110
107
 
111
108
  if detector_model == "Frame Difference":
112
109
  area = self.camera_storage.values["frame_difference"]["area"]
113
- reference_frame_frequency = self.camera_storage.values["frame_difference"][
114
- "reference_frame_frequency"
115
- ]
116
-
117
- if first_frame is None or (
118
- reference_frame_frequency
119
- and now - last_frame_time > (reference_frame_frequency * 1000)
120
- ):
121
- first_frame = image_array
122
- last_frame_time = now
110
+
111
+ if self.previous_frame is None:
112
+ self.previous_frame = image_array
123
113
  continue
124
114
 
125
115
  dets = await asyncio.get_event_loop().run_in_executor(
126
116
  self.executor,
127
117
  get_detections_fd,
128
- first_frame,
118
+ self.previous_frame,
129
119
  image_array,
130
120
  area,
131
121
  )
132
122
 
123
+ self.previous_frame = image_array
124
+
133
125
  elif detector_model == "Background Substraction":
134
126
  if backSub is None:
135
127
  backSub = cv2.createBackgroundSubtractorMOG2(varThreshold=18, detectShadows=False)
136
128
 
137
129
  threshold = self.camera_storage.values["background_substraction"]["threshold"]
138
130
  area = self.camera_storage.values["background_substraction"]["area"]
131
+ learning_rate = self.camera_storage.values["background_substraction"]["learning_rate"]
139
132
 
140
133
  dets = await asyncio.get_event_loop().run_in_executor(
141
134
  self.executor,
@@ -144,35 +137,30 @@ class CameraDetector:
144
137
  backSub,
145
138
  threshold,
146
139
  area,
140
+ learning_rate,
147
141
  )
148
142
 
149
143
  else: # default
150
144
  blur = self.camera_storage.values["default"]["blur"]
151
145
  threshold = self.camera_storage.values["default"]["threshold"]
152
146
  area = self.camera_storage.values["default"]["area"]
153
- reference_frame_frequency = self.camera_storage.values["default"][
154
- "reference_frame_frequency"
155
- ]
156
-
157
147
  image_array = cv2.stackBlur(image_array, (blur, blur))
158
148
 
159
- if first_frame is None or (
160
- reference_frame_frequency
161
- and now - last_frame_time > (reference_frame_frequency * 1000)
162
- ):
163
- first_frame = image_array
164
- last_frame_time = now
149
+ if self.previous_frame is None:
150
+ self.previous_frame = image_array
165
151
  continue
166
152
 
167
153
  dets = await asyncio.get_event_loop().run_in_executor(
168
154
  self.executor,
169
155
  get_detections,
170
- first_frame,
156
+ self.previous_frame,
171
157
  image_array,
172
158
  threshold,
173
159
  area,
174
160
  )
175
161
 
162
+ self.previous_frame = image_array
163
+
176
164
  detections: list[Detection] = []
177
165
 
178
166
  for det in dets:
@@ -192,7 +180,7 @@ class CameraDetector:
192
180
 
193
181
  await self.camera.update_state("motion", {"detections": detections}, frame)
194
182
  except asyncio.CancelledError:
195
- pass
183
+ break
196
184
  except Exception as error:
197
185
  self.logger.error(self.camera.name, "Error generating frames", error)
198
186
  self.logger.log(self.camera.name, "Restarting motion detection in 5 seconds...")
@@ -203,202 +191,183 @@ class CameraDetector:
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):