@camera.ui/camera-ui-opencl 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle.zip +0 -0
- package/package.json +15 -13
- package/CHANGELOG.md +0 -8
- package/CONTRIBUTING.md +0 -1
- package/LICENSE.md +0 -22
- package/README.md +0 -1
- package/config.schema.json +0 -3
- package/requirements.txt +0 -5
- package/src/camera_detector.py +0 -245
- package/src/detector_defaults.py +0 -5
- package/src/main.py +0 -200
- package/src/opencl.cl +0 -70
- package/src/opencl_detector.py +0 -182
- package/src/opencl_utils.py +0 -66
- package/src/plugin_typings.py +0 -8
package/bundle.zip
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "OpenCL",
|
|
3
3
|
"name": "@camera.ui/camera-ui-opencl",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"description": "camera.ui opencl plugin",
|
|
6
6
|
"author": "seydx (https://github.com/seydx/camera.ui)",
|
|
7
|
-
"main": "./
|
|
8
|
-
"type": "
|
|
7
|
+
"main": "./dist/main.py",
|
|
8
|
+
"type": "commonjs",
|
|
9
9
|
"scripts": {
|
|
10
|
+
"bundle": "npm run format && npm run lint && cui bundle",
|
|
10
11
|
"format": "ruff format",
|
|
11
12
|
"install-updates": "npm i --save",
|
|
12
|
-
"lint": "ruff check
|
|
13
|
-
"
|
|
14
|
-
"prepublishOnly": "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
13
|
+
"lint": "ruff check",
|
|
14
|
+
"lint:fix": "ruff check --fix",
|
|
15
|
+
"prepublishOnly": "node -e \"if(!process.env.SAFE_PUBLISH){console.error('Error: Please use @camera.ui/cli to publish the plugin:\\n npm run publish:alpha\\n npm run publish:beta\\n npm run publish:latest\\n');process.exit(1)}\"",
|
|
16
|
+
"publish:alpha": "cui publish --alpha",
|
|
17
|
+
"publish:beta": "cui publish --beta",
|
|
18
|
+
"publish:latest": "cui publish --latest",
|
|
19
|
+
"update": "updates --update ./"
|
|
19
20
|
},
|
|
20
21
|
"bugs": {
|
|
21
22
|
"url": "https://github.com/seydx/camera.ui/issues"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"camera.ui": ">=0.0.34-alpha.2",
|
|
25
|
-
"node": ">=20.
|
|
26
|
+
"node": ">=20.18.0"
|
|
26
27
|
},
|
|
27
28
|
"homepage": "https://github.com/seydx/camera.ui#readme",
|
|
28
29
|
"keywords": [
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"dependencies": [],
|
|
43
44
|
"options": {
|
|
44
45
|
"extendedMotionDetection": true
|
|
45
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"bundled": true
|
|
46
48
|
}
|
|
47
|
-
}
|
|
49
|
+
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
All notable changes to this project will be documented in this file.
|
|
2
|
-
|
|
3
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
4
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
-
|
|
6
|
-
## [X.X.X] - ???
|
|
7
|
-
|
|
8
|
-
- Initial Release
|
package/CONTRIBUTING.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Contributing
|
package/LICENSE.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023-2024 seydx <dev@seydx.com>
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
a copy of this software and associated documentation files (the
|
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
-
the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
included in all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# @camera.ui/camera-ui-opencl
|
package/config.schema.json
DELETED
package/requirements.txt
DELETED
package/src/camera_detector.py
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
-
from typing import Any, Optional
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
from camera_ui_python_types import (
|
|
7
|
-
CameraDevice,
|
|
8
|
-
CameraStorage,
|
|
9
|
-
Detection,
|
|
10
|
-
PluginAPI,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
14
|
-
from opencl_detector import OpenCLMotionDetector, createProgram
|
|
15
|
-
from plugin_typings import CameraStorageValues
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CameraDetector:
|
|
19
|
-
def __init__(self, api: PluginAPI, camera_device: CameraDevice) -> None:
|
|
20
|
-
super().__init__()
|
|
21
|
-
|
|
22
|
-
self.api = api
|
|
23
|
-
self.camera_device = camera_device
|
|
24
|
-
self.camera_logger = camera_device.logger
|
|
25
|
-
|
|
26
|
-
self.started = False
|
|
27
|
-
self.closed = False
|
|
28
|
-
self.restarting = False
|
|
29
|
-
|
|
30
|
-
self.initialized = False
|
|
31
|
-
|
|
32
|
-
self.executor = None
|
|
33
|
-
self.previous_frame: Optional[np.ndarray[Any, Any]] = None
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
self.ctx = createProgram()
|
|
37
|
-
except Exception as e:
|
|
38
|
-
self.camera_logger.error("Error initializing OpenCL detector", e)
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
|
|
42
|
-
|
|
43
|
-
async def initialize(self) -> None:
|
|
44
|
-
if self.initialized:
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
self.initialized = True
|
|
48
|
-
|
|
49
|
-
async def on_connected(connected: bool) -> None:
|
|
50
|
-
if connected:
|
|
51
|
-
await self.start()
|
|
52
|
-
else:
|
|
53
|
-
self.close()
|
|
54
|
-
|
|
55
|
-
await self.camera_device.on_connected.asubscribe(on_connected)
|
|
56
|
-
|
|
57
|
-
async def start(self) -> None:
|
|
58
|
-
if not self.started:
|
|
59
|
-
self.started = True
|
|
60
|
-
self.closed = False
|
|
61
|
-
|
|
62
|
-
self.executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
|
|
63
|
-
|
|
64
|
-
self.camera_logger.log("Starting motion detection")
|
|
65
|
-
|
|
66
|
-
await self.__start_detection()
|
|
67
|
-
|
|
68
|
-
def close(self) -> None:
|
|
69
|
-
if not self.closed:
|
|
70
|
-
self.started = False
|
|
71
|
-
self.closed = True
|
|
72
|
-
|
|
73
|
-
self.camera_logger.log("Stopping motion detection")
|
|
74
|
-
|
|
75
|
-
if self.executor is not None:
|
|
76
|
-
self.executor.shutdown(wait=False)
|
|
77
|
-
self.executor = None
|
|
78
|
-
|
|
79
|
-
async def __start_detection(self) -> None:
|
|
80
|
-
try:
|
|
81
|
-
await self.__detect()
|
|
82
|
-
except asyncio.CancelledError:
|
|
83
|
-
pass
|
|
84
|
-
except Exception as error:
|
|
85
|
-
self.camera_logger.error("Error generating frames", error)
|
|
86
|
-
await self.__restart()
|
|
87
|
-
|
|
88
|
-
async def __restart(self) -> None:
|
|
89
|
-
if self.restarting or self.closed:
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
self.restarting = True
|
|
93
|
-
|
|
94
|
-
self.camera_logger.log("Restarting motion detection in 2s...")
|
|
95
|
-
await asyncio.sleep(2)
|
|
96
|
-
|
|
97
|
-
self.restarting = False
|
|
98
|
-
|
|
99
|
-
if self.camera_device.connected and not self.closed:
|
|
100
|
-
await self.__start_detection()
|
|
101
|
-
|
|
102
|
-
async def __detect(self) -> None:
|
|
103
|
-
opencl_detector: Optional[OpenCLMotionDetector] = None
|
|
104
|
-
|
|
105
|
-
async for frame in self.camera_device.get_frames():
|
|
106
|
-
if self.closed:
|
|
107
|
-
break
|
|
108
|
-
|
|
109
|
-
frame_image = await frame.to_image(
|
|
110
|
-
{"format": {"to": "gray"}, "resize": {"width": 640, "height": 0}}
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
image_array = np.array(frame_image["image"])
|
|
114
|
-
|
|
115
|
-
width = frame_image["info"]["width"]
|
|
116
|
-
height = frame_image["info"]["height"]
|
|
117
|
-
blur = self.camera_storage.values["blur"]
|
|
118
|
-
threshold = self.camera_storage.values["threshold"]
|
|
119
|
-
area = self.camera_storage.values["area"]
|
|
120
|
-
dilation = self.camera_storage.values["dilation"]
|
|
121
|
-
|
|
122
|
-
if not opencl_detector:
|
|
123
|
-
opencl_detector = OpenCLMotionDetector(self.ctx, width, height, blur, self.camera_logger)
|
|
124
|
-
|
|
125
|
-
dets = await asyncio.get_event_loop().run_in_executor(
|
|
126
|
-
self.executor,
|
|
127
|
-
opencl_detector.process_frame,
|
|
128
|
-
image_array,
|
|
129
|
-
threshold,
|
|
130
|
-
dilation,
|
|
131
|
-
area,
|
|
132
|
-
default_alpha,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
detections: list[Detection] = []
|
|
136
|
-
|
|
137
|
-
for det in dets:
|
|
138
|
-
(x1, y1, x2, y2) = det
|
|
139
|
-
|
|
140
|
-
detection: Detection = {
|
|
141
|
-
"label": "motion",
|
|
142
|
-
"confidence": 1,
|
|
143
|
-
"boundingBox": (x1, y1, x2, y2),
|
|
144
|
-
"inputWidth": frame_image["info"]["width"],
|
|
145
|
-
"inputHeight": frame_image["info"]["height"],
|
|
146
|
-
"origWidth": frame.metadata["origWidth"],
|
|
147
|
-
"origHeight": frame.metadata["origHeight"],
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
detections.append(detection)
|
|
151
|
-
|
|
152
|
-
await self.camera_device.update_state("motion", {"detections": detections}, frame)
|
|
153
|
-
|
|
154
|
-
async def __default_settings(self) -> None:
|
|
155
|
-
if self.camera_storage.values["area"] != default_area:
|
|
156
|
-
await self.camera_storage.setValue("area", default_area)
|
|
157
|
-
|
|
158
|
-
if self.camera_storage.values["threshold"] != default_threshold:
|
|
159
|
-
await self.camera_storage.setValue("threshold", default_threshold)
|
|
160
|
-
|
|
161
|
-
if self.camera_storage.values["blur"] != default_blur:
|
|
162
|
-
await self.camera_storage.setValue("blur", default_blur)
|
|
163
|
-
|
|
164
|
-
if self.camera_storage.values["dilation"] != default_dilt:
|
|
165
|
-
await self.camera_storage.setValue("dilation", default_dilt)
|
|
166
|
-
|
|
167
|
-
def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
|
|
168
|
-
camera_storage: CameraStorage[CameraStorageValues] = (
|
|
169
|
-
self.api.storage_controller.create_camera_storage(
|
|
170
|
-
self,
|
|
171
|
-
self.camera_device.id,
|
|
172
|
-
{
|
|
173
|
-
"area": {
|
|
174
|
-
"type": "number",
|
|
175
|
-
"key": "area",
|
|
176
|
-
"title": "Area",
|
|
177
|
-
"description": "Minimum size of detected motion (pixels)",
|
|
178
|
-
"store": True,
|
|
179
|
-
"defaultValue": default_area,
|
|
180
|
-
"minimum": 10,
|
|
181
|
-
"maximum": 1000,
|
|
182
|
-
"step": 1,
|
|
183
|
-
"required": True,
|
|
184
|
-
"onSet": lambda new, old: self.camera_logger.log(
|
|
185
|
-
f"OpenCL model motion area changed from {old} to {new}"
|
|
186
|
-
),
|
|
187
|
-
},
|
|
188
|
-
"threshold": {
|
|
189
|
-
"type": "number",
|
|
190
|
-
"key": "threshold",
|
|
191
|
-
"title": "Threshold",
|
|
192
|
-
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
193
|
-
"store": True,
|
|
194
|
-
"defaultValue": default_threshold,
|
|
195
|
-
"minimum": 0,
|
|
196
|
-
"maximum": 1,
|
|
197
|
-
"step": 0.01,
|
|
198
|
-
"required": True,
|
|
199
|
-
"onSet": lambda new, old: self.camera_logger.log(
|
|
200
|
-
f"OpenCL model threshold changed from {old} to {new}"
|
|
201
|
-
),
|
|
202
|
-
},
|
|
203
|
-
"blur": {
|
|
204
|
-
"type": "number",
|
|
205
|
-
"key": "blur",
|
|
206
|
-
"title": "Blur",
|
|
207
|
-
"description": "Gaussian blur radius to reduce noise",
|
|
208
|
-
"store": True,
|
|
209
|
-
"defaultValue": default_blur,
|
|
210
|
-
"minimum": 1,
|
|
211
|
-
"maximum": 21,
|
|
212
|
-
"step": 1,
|
|
213
|
-
"required": True,
|
|
214
|
-
"onSet": lambda new, old: self.camera_logger.log(
|
|
215
|
-
f"OpenCL model blur radius changed from {old} to {new}"
|
|
216
|
-
),
|
|
217
|
-
},
|
|
218
|
-
"dilation": {
|
|
219
|
-
"type": "number",
|
|
220
|
-
"key": "dilation",
|
|
221
|
-
"title": "Dilation",
|
|
222
|
-
"description": "Expansion of detected motion areas",
|
|
223
|
-
"store": True,
|
|
224
|
-
"defaultValue": default_dilt,
|
|
225
|
-
"minimum": 1,
|
|
226
|
-
"maximum": 21,
|
|
227
|
-
"step": 1,
|
|
228
|
-
"required": True,
|
|
229
|
-
"onSet": lambda new, old: self.camera_logger.log(
|
|
230
|
-
self.camera_device.name,
|
|
231
|
-
f"OpenCL model dilation size changed from {old} to {new}",
|
|
232
|
-
),
|
|
233
|
-
},
|
|
234
|
-
"reset": {
|
|
235
|
-
"type": "button",
|
|
236
|
-
"title": "Default Settings",
|
|
237
|
-
"key": "default",
|
|
238
|
-
"description": "Reset motion detection settings to default",
|
|
239
|
-
"onSet": lambda new, old: self.__default_settings(),
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
)
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
return camera_storage
|
package/src/detector_defaults.py
DELETED
package/src/main.py
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import tempfile
|
|
3
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
-
from typing import Any, Union
|
|
5
|
-
|
|
6
|
-
import cv2
|
|
7
|
-
from camera_ui_python_types import (
|
|
8
|
-
CameraDevice,
|
|
9
|
-
CameraExtension,
|
|
10
|
-
FormSubmitResponse,
|
|
11
|
-
LoggerService,
|
|
12
|
-
MotionDetectionPlugin,
|
|
13
|
-
MotionDetectionPluginResponse,
|
|
14
|
-
PluginAPI,
|
|
15
|
-
RootSchema,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
from camera_detector import CameraDetector
|
|
19
|
-
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
20
|
-
from opencl_detector import OpenCLMotionDetector, createProgram
|
|
21
|
-
from opencl_utils import merge_overlapping_boxes
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class OpenCL(MotionDetectionPlugin):
|
|
25
|
-
def __init__(self, logger: LoggerService, api: PluginAPI):
|
|
26
|
-
self.api = api
|
|
27
|
-
self.logger = logger
|
|
28
|
-
self.cameras: dict[str, CameraDevice] = {}
|
|
29
|
-
self.detectors: dict[str, CameraDetector] = {}
|
|
30
|
-
|
|
31
|
-
self.api.on("finishLaunching", self.start)
|
|
32
|
-
self.api.on("shutdown", self.stop)
|
|
33
|
-
|
|
34
|
-
self.api.device_manager.on("cameraSelected", self.camera_selected)
|
|
35
|
-
self.api.device_manager.on("cameraDeselected", self.camera_deselected)
|
|
36
|
-
|
|
37
|
-
async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
|
|
38
|
-
self.logger.log(f"New camera added ({extension}): {camera.name}")
|
|
39
|
-
self.cameras[camera.id] = camera
|
|
40
|
-
await self.create_detector(camera)
|
|
41
|
-
|
|
42
|
-
def camera_deselected(self, id: str, extension: CameraExtension) -> None:
|
|
43
|
-
camera = self.cameras.get(id)
|
|
44
|
-
|
|
45
|
-
if camera:
|
|
46
|
-
self.logger.log(f"Camera removed ({extension}): {camera.name}")
|
|
47
|
-
self.remove_detector(self.cameras[id])
|
|
48
|
-
del self.cameras[id]
|
|
49
|
-
|
|
50
|
-
async def start(self) -> None:
|
|
51
|
-
self.logger.log("Plugin Started")
|
|
52
|
-
|
|
53
|
-
await asyncio.gather(*[self.create_detector(camera) for camera in self.cameras.values()])
|
|
54
|
-
|
|
55
|
-
def stop(self) -> None:
|
|
56
|
-
self.logger.log("Plugin Stopped")
|
|
57
|
-
|
|
58
|
-
detectors_copy = list(self.detectors.values())
|
|
59
|
-
|
|
60
|
-
for detector in detectors_copy:
|
|
61
|
-
self.remove_detector(detector.camera_device)
|
|
62
|
-
|
|
63
|
-
async def onFormSubmit(self, action_id: str, payload: Any) -> Union[FormSubmitResponse, None]:
|
|
64
|
-
print("Form submitted", action_id, payload)
|
|
65
|
-
|
|
66
|
-
def configureCameras(self, cameras: list[CameraDevice]) -> None:
|
|
67
|
-
for camera in cameras:
|
|
68
|
-
self.cameras[camera.id] = camera
|
|
69
|
-
|
|
70
|
-
async def create_detector(self, camera: CameraDevice) -> None:
|
|
71
|
-
detector = CameraDetector(self.api, camera)
|
|
72
|
-
self.detectors[camera.id] = detector
|
|
73
|
-
await detector.initialize()
|
|
74
|
-
|
|
75
|
-
def remove_detector(self, camera: CameraDevice) -> None:
|
|
76
|
-
detector = self.detectors[camera.id]
|
|
77
|
-
|
|
78
|
-
if detector:
|
|
79
|
-
detector.close()
|
|
80
|
-
del self.detectors[camera.id]
|
|
81
|
-
|
|
82
|
-
def interfaceSchema(self) -> RootSchema:
|
|
83
|
-
root_schema: RootSchema = {
|
|
84
|
-
"schema": {
|
|
85
|
-
"area": {
|
|
86
|
-
"type": "number",
|
|
87
|
-
"key": "area",
|
|
88
|
-
"title": "Area",
|
|
89
|
-
"description": "Minimum size of detected motion (pixels)",
|
|
90
|
-
"store": False,
|
|
91
|
-
"defaultValue": default_area,
|
|
92
|
-
"minimum": 10,
|
|
93
|
-
"maximum": 1000,
|
|
94
|
-
"step": 1,
|
|
95
|
-
"required": True,
|
|
96
|
-
},
|
|
97
|
-
"threshold": {
|
|
98
|
-
"type": "number",
|
|
99
|
-
"key": "threshold",
|
|
100
|
-
"title": "Threshold",
|
|
101
|
-
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
102
|
-
"store": False,
|
|
103
|
-
"defaultValue": default_threshold,
|
|
104
|
-
"minimum": 0,
|
|
105
|
-
"maximum": 1,
|
|
106
|
-
"step": 0.01,
|
|
107
|
-
"required": True,
|
|
108
|
-
},
|
|
109
|
-
"blur": {
|
|
110
|
-
"type": "number",
|
|
111
|
-
"key": "blur",
|
|
112
|
-
"title": "Blur",
|
|
113
|
-
"description": "Gaussian blur radius to reduce noise",
|
|
114
|
-
"store": False,
|
|
115
|
-
"defaultValue": default_blur,
|
|
116
|
-
"minimum": 1,
|
|
117
|
-
"maximum": 21,
|
|
118
|
-
"step": 1,
|
|
119
|
-
"required": True,
|
|
120
|
-
},
|
|
121
|
-
"dilation": {
|
|
122
|
-
"type": "number",
|
|
123
|
-
"key": "dilation",
|
|
124
|
-
"title": "Dilation",
|
|
125
|
-
"description": "Expansion of detected motion areas",
|
|
126
|
-
"store": False,
|
|
127
|
-
"defaultValue": default_dilt,
|
|
128
|
-
"minimum": 1,
|
|
129
|
-
"maximum": 21,
|
|
130
|
-
"step": 1,
|
|
131
|
-
"required": True,
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return root_schema
|
|
137
|
-
|
|
138
|
-
async def detectMotion(self, video_path: str, config: dict[str, Any]) -> MotionDetectionPluginResponse:
|
|
139
|
-
ctx = createProgram()
|
|
140
|
-
|
|
141
|
-
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
|
|
142
|
-
output_file = temp_file.name
|
|
143
|
-
|
|
144
|
-
cap = cv2.VideoCapture(video_path)
|
|
145
|
-
executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
|
|
146
|
-
|
|
147
|
-
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
148
|
-
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
149
|
-
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
|
150
|
-
|
|
151
|
-
fourcc = cv2.VideoWriter.fourcc("a", "v", "c", "1")
|
|
152
|
-
out = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
|
|
153
|
-
|
|
154
|
-
blur = config.get("blur", default_blur)
|
|
155
|
-
threshold = config.get("threshold", default_threshold)
|
|
156
|
-
area = config.get("area", default_area)
|
|
157
|
-
dilation = config.get("dilation", default_dilt)
|
|
158
|
-
|
|
159
|
-
opencl_detector = OpenCLMotionDetector(ctx, width, height, blur, self.logger)
|
|
160
|
-
|
|
161
|
-
while cap.isOpened():
|
|
162
|
-
ret, frame = cap.read()
|
|
163
|
-
if not ret:
|
|
164
|
-
break
|
|
165
|
-
|
|
166
|
-
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
167
|
-
|
|
168
|
-
dets = await asyncio.get_event_loop().run_in_executor(
|
|
169
|
-
executor,
|
|
170
|
-
opencl_detector.process_frame,
|
|
171
|
-
gray,
|
|
172
|
-
threshold,
|
|
173
|
-
dilation,
|
|
174
|
-
area,
|
|
175
|
-
default_alpha,
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
merged_boxed = merge_overlapping_boxes(dets)
|
|
179
|
-
|
|
180
|
-
for det in merged_boxed:
|
|
181
|
-
(x1, y1, x2, y2) = det
|
|
182
|
-
|
|
183
|
-
pt1: cv2.typing.Point = (int(x1), int(y1))
|
|
184
|
-
pt2: cv2.typing.Point = (int(x2), int(y2))
|
|
185
|
-
|
|
186
|
-
cv2.rectangle(frame, pt1, pt2, (0, 255, 0), 2)
|
|
187
|
-
|
|
188
|
-
out.write(frame)
|
|
189
|
-
|
|
190
|
-
cap.release()
|
|
191
|
-
out.release()
|
|
192
|
-
executor.shutdown()
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
"filePath": output_file,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def __main__():
|
|
200
|
-
return OpenCL
|
package/src/opencl.cl
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
|
|
2
|
-
|
|
3
|
-
__kernel void process_frame(
|
|
4
|
-
__read_only image2d_t input,
|
|
5
|
-
__global float *background_model,
|
|
6
|
-
__global float *temp_buffer,
|
|
7
|
-
__write_only image2d_t output,
|
|
8
|
-
__constant float *kernel_values,
|
|
9
|
-
int kernel_size,
|
|
10
|
-
int width,
|
|
11
|
-
int height,
|
|
12
|
-
float alpha,
|
|
13
|
-
float threshold,
|
|
14
|
-
int dilation_size,
|
|
15
|
-
int first_frame
|
|
16
|
-
) {
|
|
17
|
-
int x = get_global_id(0);
|
|
18
|
-
int y = get_global_id(1);
|
|
19
|
-
if (x >= width || y >= height) return;
|
|
20
|
-
int idx = y * width + x;
|
|
21
|
-
|
|
22
|
-
// Gaussian blur
|
|
23
|
-
int radius = (kernel_size - 1) / 2;
|
|
24
|
-
float blurred_value = 0.0f;
|
|
25
|
-
for (int i = -radius; i <= radius; i++) {
|
|
26
|
-
for (int j = -radius; j <= radius; j++) {
|
|
27
|
-
int2 samplePos = (int2)(clamp(x + i, 0, width - 1), clamp(y + j, 0, height - 1));
|
|
28
|
-
float4 pixel = read_imagef(input, sampler, samplePos);
|
|
29
|
-
float weight = kernel_values[i + radius] * kernel_values[j + radius];
|
|
30
|
-
blurred_value += pixel.x * weight;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Store blurred value in temp buffer
|
|
35
|
-
temp_buffer[idx] = blurred_value;
|
|
36
|
-
|
|
37
|
-
if (first_frame) {
|
|
38
|
-
// Initialize background model
|
|
39
|
-
background_model[idx] = blurred_value;
|
|
40
|
-
write_imagef(output, (int2)(x, y), (float4)(0.0f, 0.0f, 0.0f, 1.0f));
|
|
41
|
-
} else {
|
|
42
|
-
// Background subtraction
|
|
43
|
-
float bg_pixel = background_model[idx];
|
|
44
|
-
|
|
45
|
-
// Update background model
|
|
46
|
-
float updated_bg = bg_pixel * (1.0f - alpha) + blurred_value * alpha;
|
|
47
|
-
background_model[idx] = updated_bg;
|
|
48
|
-
|
|
49
|
-
// Compute difference and threshold
|
|
50
|
-
float diff = fabs(blurred_value - bg_pixel);
|
|
51
|
-
float mask_value = (diff > threshold) ? 1.0f : 0.0f;
|
|
52
|
-
|
|
53
|
-
// Dilation
|
|
54
|
-
float maxValue = mask_value;
|
|
55
|
-
for (int dy = -dilation_size; dy <= dilation_size; dy++) {
|
|
56
|
-
int sy = clamp(y + dy, 0, height - 1);
|
|
57
|
-
for (int dx = -dilation_size; dx <= dilation_size; dx++) {
|
|
58
|
-
int sx = clamp(x + dx, 0, width - 1);
|
|
59
|
-
int neighbor_idx = sy * width + sx;
|
|
60
|
-
float neighbor_pixel = background_model[neighbor_idx];
|
|
61
|
-
float neighbor_blurred = temp_buffer[neighbor_idx];
|
|
62
|
-
float neighbor_diff = fabs(neighbor_blurred - neighbor_pixel);
|
|
63
|
-
float neighbor_mask = (neighbor_diff > threshold) ? 1.0f : 0.0f;
|
|
64
|
-
maxValue = fmax(maxValue, neighbor_mask);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
write_imagef(output, (int2)(x, y), (float4)(maxValue, maxValue, maxValue, 1.0f));
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/opencl_detector.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pyopencl as cl
|
|
6
|
-
from camera_ui_python_types import LoggerService
|
|
7
|
-
|
|
8
|
-
from opencl_utils import get_contour_detections
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def createProgram() -> Any:
|
|
12
|
-
platforms: Any = cl.get_platforms()
|
|
13
|
-
|
|
14
|
-
if len(platforms) == 0:
|
|
15
|
-
raise RuntimeError("Failed to find any OpenCL platforms.")
|
|
16
|
-
|
|
17
|
-
devices = platforms[0].get_devices(cl.device_type.GPU)
|
|
18
|
-
if len(devices) == 0:
|
|
19
|
-
devices = platforms[0].get_devices(cl.device_type.CPU)
|
|
20
|
-
if len(devices) == 0:
|
|
21
|
-
raise RuntimeError("Could not find OpenCL GPU or CPU device.")
|
|
22
|
-
|
|
23
|
-
device = devices[0]
|
|
24
|
-
context: Any = cl.Context([device])
|
|
25
|
-
|
|
26
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
-
full_path = os.path.join(current_dir, "opencl.cl")
|
|
28
|
-
|
|
29
|
-
with open(full_path) as kernelFile:
|
|
30
|
-
kernelStr = kernelFile.read()
|
|
31
|
-
|
|
32
|
-
program = cl.Program(context, kernelStr)
|
|
33
|
-
program.build(devices=[device])
|
|
34
|
-
|
|
35
|
-
return context, device, program
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class OpenCLMotionDetector:
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
ctx: tuple[Any, Any, Any],
|
|
42
|
-
width: int,
|
|
43
|
-
height: int,
|
|
44
|
-
blur_radius: int,
|
|
45
|
-
camera_logger: LoggerService,
|
|
46
|
-
):
|
|
47
|
-
self.camera_logger = camera_logger
|
|
48
|
-
self.width = width
|
|
49
|
-
self.height = height
|
|
50
|
-
|
|
51
|
-
# OpenCL Setup
|
|
52
|
-
self.ctx: Any = ctx[0]
|
|
53
|
-
self.device: Any = ctx[1]
|
|
54
|
-
self.program: Any = ctx[2]
|
|
55
|
-
self.queue: Any = cl.CommandQueue(self.ctx, properties=cl.command_queue_properties.PROFILING_ENABLE)
|
|
56
|
-
|
|
57
|
-
mf: Any = cl.mem_flags
|
|
58
|
-
|
|
59
|
-
# Create reusable OpenCL images and buffers
|
|
60
|
-
self.input_image: Any = cl.Image(
|
|
61
|
-
self.ctx,
|
|
62
|
-
mf.READ_ONLY,
|
|
63
|
-
cl.ImageFormat(cl.channel_order.R, cl.channel_type.UNORM_INT8),
|
|
64
|
-
shape=(self.width, self.height),
|
|
65
|
-
)
|
|
66
|
-
self.result_image: Any = cl.Image(
|
|
67
|
-
self.ctx,
|
|
68
|
-
mf.WRITE_ONLY,
|
|
69
|
-
cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
70
|
-
shape=(self.width, self.height),
|
|
71
|
-
)
|
|
72
|
-
self.background_model_buf: Any = cl.Buffer(
|
|
73
|
-
self.ctx, mf.READ_WRITE, size=self.width * self.height * np.dtype(np.float32).itemsize
|
|
74
|
-
)
|
|
75
|
-
self.temp_buffer: Any = cl.Buffer(
|
|
76
|
-
self.ctx, mf.READ_WRITE, size=self.width * self.height * np.dtype(np.float32).itemsize
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
# Create host buffer for results
|
|
80
|
-
self.host_result_buffer = np.zeros((self.height, self.width), dtype=np.float32)
|
|
81
|
-
|
|
82
|
-
# Create Gaussian kernel and copy it to global GPU memory
|
|
83
|
-
self.kernel = self.__create_gaussian_kernel(blur_radius)
|
|
84
|
-
self.kernel_buf: Any = cl.Buffer(self.ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=self.kernel)
|
|
85
|
-
self.kernel_size = np.int32(len(self.kernel))
|
|
86
|
-
|
|
87
|
-
max_work_group_size = self.device.get_info(cl.device_info.MAX_WORK_GROUP_SIZE)
|
|
88
|
-
compute_units = self.device.get_info(cl.device_info.MAX_COMPUTE_UNITS)
|
|
89
|
-
|
|
90
|
-
self.local_size = self.__get_optimal_work_group_size(max_work_group_size, compute_units)
|
|
91
|
-
self.global_size = (
|
|
92
|
-
((self.width + self.local_size[0] - 1) // self.local_size[0]) * self.local_size[0],
|
|
93
|
-
((self.height + self.local_size[1] - 1) // self.local_size[1]) * self.local_size[1],
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
self.first_frame = True
|
|
97
|
-
|
|
98
|
-
self.camera_logger.debug(
|
|
99
|
-
{
|
|
100
|
-
"device_name": self.device.name,
|
|
101
|
-
"opencl_version": self.device.version,
|
|
102
|
-
"compute_units": compute_units,
|
|
103
|
-
"local_work_group_size": self.local_size,
|
|
104
|
-
},
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
def process_frame(
|
|
108
|
-
self,
|
|
109
|
-
gray_frame: np.ndarray[Any, Any],
|
|
110
|
-
motion_threshold: float,
|
|
111
|
-
dilation_size: int,
|
|
112
|
-
area_threshold: int,
|
|
113
|
-
alpha: float,
|
|
114
|
-
) -> list[tuple[float, float, float, float]]:
|
|
115
|
-
# Copy the frame to OpenCL image
|
|
116
|
-
cl.enqueue_copy(
|
|
117
|
-
self.queue,
|
|
118
|
-
self.input_image,
|
|
119
|
-
gray_frame,
|
|
120
|
-
origin=(0, 0),
|
|
121
|
-
region=(self.width, self.height),
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
# Process frame
|
|
125
|
-
event = self.program.process_frame(
|
|
126
|
-
self.queue,
|
|
127
|
-
self.global_size,
|
|
128
|
-
self.local_size,
|
|
129
|
-
self.input_image,
|
|
130
|
-
self.background_model_buf,
|
|
131
|
-
self.temp_buffer,
|
|
132
|
-
self.result_image,
|
|
133
|
-
self.kernel_buf,
|
|
134
|
-
self.kernel_size,
|
|
135
|
-
np.int32(self.width),
|
|
136
|
-
np.int32(self.height),
|
|
137
|
-
np.float32(alpha),
|
|
138
|
-
np.float32(motion_threshold),
|
|
139
|
-
np.int32(dilation_size),
|
|
140
|
-
np.int32(self.first_frame),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
# Copy results back to host
|
|
144
|
-
cl.enqueue_copy(
|
|
145
|
-
self.queue,
|
|
146
|
-
self.host_result_buffer,
|
|
147
|
-
self.result_image,
|
|
148
|
-
origin=(0, 0),
|
|
149
|
-
region=(self.width, self.height),
|
|
150
|
-
wait_for=[event],
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
motion_mask_cv = (self.host_result_buffer * 255).astype(np.uint8)
|
|
154
|
-
detections = get_contour_detections(motion_mask_cv, area_threshold)
|
|
155
|
-
|
|
156
|
-
self.first_frame = False
|
|
157
|
-
return detections
|
|
158
|
-
|
|
159
|
-
def __get_optimal_work_group_size(self, max_group: int, compute_units: int) -> tuple[int, int]:
|
|
160
|
-
optimal_group_count = compute_units * 4
|
|
161
|
-
|
|
162
|
-
for size in [32, 16, 8, 4]:
|
|
163
|
-
work_groups_per_dimension = (self.width // size) * (self.height // size)
|
|
164
|
-
if (
|
|
165
|
-
self.width % size == 0
|
|
166
|
-
and self.height % size == 0
|
|
167
|
-
and size * size <= max_group
|
|
168
|
-
and work_groups_per_dimension >= optimal_group_count
|
|
169
|
-
):
|
|
170
|
-
return (size, size)
|
|
171
|
-
|
|
172
|
-
return (min(max_group, self.width), 1)
|
|
173
|
-
|
|
174
|
-
def __create_gaussian_kernel(self, radius: int) -> np.ndarray[Any, Any]:
|
|
175
|
-
sigma = radius / 3.0
|
|
176
|
-
x = np.arange(-radius, radius + 1)
|
|
177
|
-
kernel = np.exp(-(x**2) / (2 * sigma**2))
|
|
178
|
-
return (kernel / np.sum(kernel)).astype(np.float32)
|
|
179
|
-
|
|
180
|
-
def __del__(self):
|
|
181
|
-
# Clean up OpenCL resources
|
|
182
|
-
self.queue.finish()
|
package/src/opencl_utils.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
import cv2
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def get_contour_detections(
|
|
8
|
-
mask: np.ndarray[Any, Any], area_threshold: int
|
|
9
|
-
) -> list[tuple[float, float, float, float]]:
|
|
10
|
-
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
11
|
-
return [
|
|
12
|
-
(float(x), float(y), float(x + w), float(y + h))
|
|
13
|
-
for contour in contours
|
|
14
|
-
if (_ := cv2.contourArea(contour)) > area_threshold
|
|
15
|
-
for x, y, w, h in [cv2.boundingRect(contour)]
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def merge_overlapping_boxes(
|
|
20
|
-
boxes: list[tuple[float, float, float, float]], overlap_threshold: float = 0.1
|
|
21
|
-
) -> list[tuple[float, float, float, float]]:
|
|
22
|
-
if not boxes:
|
|
23
|
-
return []
|
|
24
|
-
|
|
25
|
-
boxes_array = np.array(boxes)
|
|
26
|
-
merged_boxes: list[tuple[float, float, float, float]] = []
|
|
27
|
-
|
|
28
|
-
while len(boxes_array) > 0:
|
|
29
|
-
box = boxes_array[0]
|
|
30
|
-
boxes_array: np.ndarray[Any, Any] = boxes_array[1:]
|
|
31
|
-
|
|
32
|
-
x1, y1, w1, h1 = box
|
|
33
|
-
x2 = x1 + w1
|
|
34
|
-
y2 = y1 + h1
|
|
35
|
-
|
|
36
|
-
indices_to_remove: list[int] = []
|
|
37
|
-
for i, (x1o, y1o, w1o, h1o) in enumerate(boxes_array):
|
|
38
|
-
x2o = x1o + w1o
|
|
39
|
-
y2o = y1o + h1o
|
|
40
|
-
|
|
41
|
-
xx1 = max(x1, x1o)
|
|
42
|
-
yy1 = max(y1, y1o)
|
|
43
|
-
xx2 = min(x2, x2o)
|
|
44
|
-
yy2 = min(y2, y2o)
|
|
45
|
-
|
|
46
|
-
w_overlap = max(0, xx2 - xx1)
|
|
47
|
-
h_overlap = max(0, yy2 - yy1)
|
|
48
|
-
overlap_area = w_overlap * h_overlap
|
|
49
|
-
area1 = w1 * h1
|
|
50
|
-
area2 = w1o * h1o
|
|
51
|
-
|
|
52
|
-
if area1 == 0 or area2 == 0:
|
|
53
|
-
continue
|
|
54
|
-
overlap_ratio = overlap_area / min(area1, area2)
|
|
55
|
-
|
|
56
|
-
if overlap_ratio > overlap_threshold:
|
|
57
|
-
x1 = min(x1, x1o)
|
|
58
|
-
y1 = min(y1, y1o)
|
|
59
|
-
x2 = max(x2, x2o)
|
|
60
|
-
y2 = max(y2, y2o)
|
|
61
|
-
indices_to_remove.append(i)
|
|
62
|
-
|
|
63
|
-
boxes_array = np.delete(boxes_array, indices_to_remove, axis=0)
|
|
64
|
-
merged_boxes.append((int(x1), int(y1), int(x2 - x1), int(y2 - y1)))
|
|
65
|
-
|
|
66
|
-
return merged_boxes
|