@camera.ui/camera-ui-opencv 0.0.9 → 0.0.11
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 +2 -2
- package/requirements.txt +1 -1
- package/src/camera_detector.py +195 -225
- package/src/detector_defaults.py +11 -8
- package/src/main.py +65 -80
- package/src/opencv_utils.py +6 -92
- package/src/plugin_typings.py +3 -2
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.
|
|
4
|
+
"version": "0.0.11",
|
|
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.
|
|
24
|
+
"camera.ui": ">=0.0.30-alpha.0",
|
|
25
25
|
"node": ">=18.12.0"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/seydx/camera.ui#readme",
|
package/requirements.txt
CHANGED
package/src/camera_detector.py
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
207
|
-
self.api.storage_controller.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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")
|
package/src/detector_defaults.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
default_area = 350
|
|
2
|
+
default_area_fd = 350
|
|
3
|
+
default_area_bs = 350
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
default_threshold = 50
|
|
6
|
+
default_threshold_bs = 50
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
default_blur = 11
|
|
9
|
+
default_dilt = 15
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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":
|
|
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
|
-
"
|
|
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
|
|
117
|
+
"description": "Minimum size of detected motion (pixels)",
|
|
114
118
|
"store": False,
|
|
115
|
-
"defaultValue":
|
|
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": "
|
|
129
|
+
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
126
130
|
"store": False,
|
|
127
|
-
"defaultValue":
|
|
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": "
|
|
141
|
+
"description": "Gaussian blur radius to reduce noise",
|
|
138
142
|
"store": False,
|
|
139
|
-
"defaultValue":
|
|
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": "
|
|
153
|
+
"description": "Expansion of detected motion areas",
|
|
150
154
|
"store": False,
|
|
151
|
-
"defaultValue":
|
|
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
|
-
"
|
|
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
|
|
176
|
+
"description": "Minimum size of detected motion (pixels)",
|
|
183
177
|
"store": False,
|
|
184
|
-
"defaultValue":
|
|
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": "
|
|
188
|
+
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
195
189
|
"store": False,
|
|
196
|
-
"defaultValue":
|
|
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
|
-
"
|
|
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
|
|
223
|
+
"description": "Minimum size of detected motion (pixels)",
|
|
216
224
|
"store": False,
|
|
217
|
-
"defaultValue":
|
|
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
|
-
|
|
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",
|
|
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
|
|
275
|
-
|
|
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
|
-
|
|
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",
|
|
294
|
-
area = config.get("background_substraction", {}).get("area",
|
|
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",
|
|
307
|
-
threshold = config.get("default", {}).get("threshold",
|
|
308
|
-
area = config.get("default", {}).get("area",
|
|
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
|
|
316
|
-
|
|
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
|
-
|
|
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
|
|
package/src/opencv_utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
return merge_bboxes(detections)
|
|
86
|
+
return get_contour_detections(motion_mask, area_threshold)
|
package/src/plugin_typings.py
CHANGED
|
@@ -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):
|