@camera.ui/camera-ui-opencl 0.0.1
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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -0
- package/LICENSE.md +22 -0
- package/README.md +1 -0
- package/config.schema.json +3 -0
- package/package.json +44 -0
- package/requirements.txt +5 -0
- package/src/camera_detector.py +213 -0
- package/src/detector_defaults.py +5 -0
- package/src/main.py +199 -0
- package/src/opencl.cl +101 -0
- package/src/opencl_detector.py +195 -0
- package/src/opencl_utils.py +69 -0
- package/src/plugin_typings.py +8 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Contributing
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @camera.ui/camera-ui-opencl
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"displayName": "OpenCL",
|
|
3
|
+
"name": "@camera.ui/camera-ui-opencl",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "camera.ui opencl plugin",
|
|
6
|
+
"author": "seydx (https://github.com/seydx/camera.ui)",
|
|
7
|
+
"main": "./src/main.py",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"format": "ruff format",
|
|
11
|
+
"install-updates": "npm i --save",
|
|
12
|
+
"lint": "ruff check --fix",
|
|
13
|
+
"update": "updates --update ./",
|
|
14
|
+
"prepublishOnly": "npm i --package-lock-only && npm run format && npm run lint"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"updates": "^16.4.0"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/seydx/camera.ui/issues"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"camera.ui": ">=0.0.34-alpha.0",
|
|
25
|
+
"node": ">=20.17.0"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/seydx/camera.ui#readme",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"camera-ui-plugin",
|
|
30
|
+
"motion",
|
|
31
|
+
"opencl",
|
|
32
|
+
"detection"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/seydx/camera.ui.git"
|
|
38
|
+
},
|
|
39
|
+
"camera.ui": {
|
|
40
|
+
"pythonVersion": "3.11",
|
|
41
|
+
"extension": "motionDetection",
|
|
42
|
+
"dependencies": []
|
|
43
|
+
}
|
|
44
|
+
}
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
LoggerService,
|
|
11
|
+
PluginAPI,
|
|
12
|
+
)
|
|
13
|
+
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
14
|
+
from opencl_detector import OpenCLMotionDetector
|
|
15
|
+
from plugin_typings import CameraStorageValues
|
|
16
|
+
|
|
17
|
+
background_tasks = set[asyncio.Task[Any]]()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CameraDetector:
|
|
21
|
+
def __init__(self, api: PluginAPI, camera: CameraDevice, logger: LoggerService) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
self.api = api
|
|
25
|
+
self.camera = camera
|
|
26
|
+
self.logger = logger
|
|
27
|
+
|
|
28
|
+
self.started = False
|
|
29
|
+
self.closed = False
|
|
30
|
+
self.frame_generation_task: Optional[asyncio.Task[Any]] = None
|
|
31
|
+
self.executor = None
|
|
32
|
+
self.previous_frame: Optional[np.ndarray[Any, Any]] = None
|
|
33
|
+
|
|
34
|
+
self.camera_storage: CameraStorage[CameraStorageValues] = self.__create_camera_storage()
|
|
35
|
+
self.camera.on_connected.subscribe(lambda connected: self.start() if connected else self.close())
|
|
36
|
+
|
|
37
|
+
def start(self) -> None:
|
|
38
|
+
if not self.started:
|
|
39
|
+
self.started = True
|
|
40
|
+
self.closed = False
|
|
41
|
+
|
|
42
|
+
self.executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
|
|
43
|
+
|
|
44
|
+
self.logger.log(f"Starting motion detection for camera {self.camera.name})...")
|
|
45
|
+
|
|
46
|
+
self.frame_generation_task = asyncio.create_task(
|
|
47
|
+
self.__detect(), name=f"MotionDetection-{self.camera.name}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
background_tasks.add(self.frame_generation_task)
|
|
51
|
+
self.frame_generation_task.add_done_callback(
|
|
52
|
+
lambda _: background_tasks.remove(self.frame_generation_task)
|
|
53
|
+
if self.frame_generation_task in background_tasks
|
|
54
|
+
else None
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def close(self) -> None:
|
|
58
|
+
if self.started and not self.closed:
|
|
59
|
+
self.started = False
|
|
60
|
+
self.closed = True
|
|
61
|
+
|
|
62
|
+
self.logger.log(f"Stopping motion detection for camera {self.camera.name}...")
|
|
63
|
+
|
|
64
|
+
if self.frame_generation_task is not None:
|
|
65
|
+
self.frame_generation_task.cancel()
|
|
66
|
+
self.frame_generation_task = None
|
|
67
|
+
|
|
68
|
+
if self.executor is not None:
|
|
69
|
+
self.executor.shutdown(wait=False)
|
|
70
|
+
self.executor = None
|
|
71
|
+
|
|
72
|
+
def restart(self) -> None:
|
|
73
|
+
self.logger.log(f"Restarting motion detection for camera {self.camera.name}...")
|
|
74
|
+
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
if self.camera.connected:
|
|
78
|
+
self.start()
|
|
79
|
+
|
|
80
|
+
async def __detect(self) -> None:
|
|
81
|
+
while not self.closed:
|
|
82
|
+
try:
|
|
83
|
+
opencl_detector: Optional[OpenCLMotionDetector] = None
|
|
84
|
+
|
|
85
|
+
async for frame in self.camera.get_frames():
|
|
86
|
+
if self.closed:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
frame_image = await frame.to_image({"format": {"to": "gray"}})
|
|
90
|
+
image_array = np.array(frame_image["image"])
|
|
91
|
+
|
|
92
|
+
width = frame_image["info"]["width"]
|
|
93
|
+
height = frame_image["info"]["height"]
|
|
94
|
+
blur = self.camera_storage.values["blur"]
|
|
95
|
+
threshold = self.camera_storage.values["threshold"]
|
|
96
|
+
area = self.camera_storage.values["area"]
|
|
97
|
+
dilation = self.camera_storage.values["dilation"]
|
|
98
|
+
|
|
99
|
+
if opencl_detector is None:
|
|
100
|
+
try:
|
|
101
|
+
opencl_detector = OpenCLMotionDetector(self.logger, width, height, blur)
|
|
102
|
+
except Exception as error:
|
|
103
|
+
self.logger.error(self.camera.name, "Error initializing OpenCL detector", error)
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
dets = await asyncio.get_event_loop().run_in_executor(
|
|
107
|
+
self.executor,
|
|
108
|
+
opencl_detector.process_frame,
|
|
109
|
+
image_array,
|
|
110
|
+
threshold,
|
|
111
|
+
dilation,
|
|
112
|
+
area,
|
|
113
|
+
default_alpha,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
detections: list[Detection] = []
|
|
117
|
+
|
|
118
|
+
for det in dets:
|
|
119
|
+
(x1, y1, x2, y2) = det
|
|
120
|
+
|
|
121
|
+
detection: Detection = {
|
|
122
|
+
"label": "motion",
|
|
123
|
+
"confidence": 1,
|
|
124
|
+
"boundingBox": (x1, y1, x2, y2),
|
|
125
|
+
"inputWidth": frame_image["info"]["width"],
|
|
126
|
+
"inputHeight": frame_image["info"]["height"],
|
|
127
|
+
"origWidth": frame.metadata["origWidth"],
|
|
128
|
+
"origHeight": frame.metadata["origHeight"],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
detections.append(detection)
|
|
132
|
+
|
|
133
|
+
await self.camera.update_state("motion", {"detections": detections}, frame)
|
|
134
|
+
except asyncio.CancelledError:
|
|
135
|
+
break
|
|
136
|
+
except Exception as error:
|
|
137
|
+
self.logger.error(self.camera.name, "Error generating frames", error)
|
|
138
|
+
self.logger.log(self.camera.name, "Restarting motion detection in 5 seconds...")
|
|
139
|
+
await asyncio.sleep(5)
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
|
|
143
|
+
camera_storage: CameraStorage[CameraStorageValues] = (
|
|
144
|
+
self.api.storage_controller.create_camera_storage(
|
|
145
|
+
self,
|
|
146
|
+
self.camera.id,
|
|
147
|
+
{
|
|
148
|
+
"area": {
|
|
149
|
+
"type": "number",
|
|
150
|
+
"key": "area",
|
|
151
|
+
"title": "Area",
|
|
152
|
+
"description": "Minimum size of detected motion (pixels)",
|
|
153
|
+
"store": True,
|
|
154
|
+
"defaultValue": default_area,
|
|
155
|
+
"minimum": 10,
|
|
156
|
+
"maximum": 1000,
|
|
157
|
+
"step": 1,
|
|
158
|
+
"required": True,
|
|
159
|
+
"onSet": lambda old, new: self.logger.log(
|
|
160
|
+
self.camera.name, f"OpenCL model motion area changed from {old} to {new}"
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
"threshold": {
|
|
164
|
+
"type": "number",
|
|
165
|
+
"key": "threshold",
|
|
166
|
+
"title": "Threshold",
|
|
167
|
+
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
168
|
+
"store": True,
|
|
169
|
+
"defaultValue": default_threshold,
|
|
170
|
+
"minimum": 0,
|
|
171
|
+
"maximum": 1,
|
|
172
|
+
"step": 0.01,
|
|
173
|
+
"required": True,
|
|
174
|
+
"onSet": lambda old, new: self.logger.log(
|
|
175
|
+
self.camera.name, f"OpenCL model threshold changed from {old} to {new}"
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
"blur": {
|
|
179
|
+
"type": "number",
|
|
180
|
+
"key": "blur",
|
|
181
|
+
"title": "Blur",
|
|
182
|
+
"description": "Gaussian blur radius to reduce noise",
|
|
183
|
+
"store": True,
|
|
184
|
+
"defaultValue": default_blur,
|
|
185
|
+
"minimum": 1,
|
|
186
|
+
"maximum": 21,
|
|
187
|
+
"step": 1,
|
|
188
|
+
"required": True,
|
|
189
|
+
"onSet": lambda old, new: self.logger.log(
|
|
190
|
+
self.camera.name, f"OpenCL model blur radius changed from {old} to {new}"
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
"dilation": {
|
|
194
|
+
"type": "number",
|
|
195
|
+
"key": "dilation",
|
|
196
|
+
"title": "Dilation",
|
|
197
|
+
"description": "Expansion of detected motion areas",
|
|
198
|
+
"store": True,
|
|
199
|
+
"defaultValue": default_dilt,
|
|
200
|
+
"minimum": 1,
|
|
201
|
+
"maximum": 21,
|
|
202
|
+
"step": 1,
|
|
203
|
+
"required": True,
|
|
204
|
+
"onSet": lambda old, new: self.logger.log(
|
|
205
|
+
self.camera.name,
|
|
206
|
+
f"OpenCL model dilation size changed from {old} to {new}",
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return camera_storage
|
package/src/main.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import tempfile
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
from camera_detector import CameraDetector
|
|
8
|
+
from camera_ui_python_types import (
|
|
9
|
+
CameraDevice,
|
|
10
|
+
CameraExtension,
|
|
11
|
+
FormSubmitResponse,
|
|
12
|
+
LoggerService,
|
|
13
|
+
MotionDetectionPlugin,
|
|
14
|
+
MotionDetectionPluginResponse,
|
|
15
|
+
PluginAPI,
|
|
16
|
+
RootSchema,
|
|
17
|
+
)
|
|
18
|
+
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
19
|
+
from opencl_detector import OpenCLMotionDetector
|
|
20
|
+
from opencl_utils import merge_overlapping_boxes
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OpenCL(MotionDetectionPlugin):
|
|
24
|
+
def __init__(self, logger: LoggerService, api: PluginAPI):
|
|
25
|
+
self.api = api
|
|
26
|
+
self.logger = logger
|
|
27
|
+
self.cameras: dict[str, CameraDevice] = {}
|
|
28
|
+
self.detectors: dict[str, CameraDetector] = {}
|
|
29
|
+
|
|
30
|
+
self.api.on("finishLaunching", self.start)
|
|
31
|
+
self.api.on("shutdown", self.stop)
|
|
32
|
+
|
|
33
|
+
self.api.device_manager.on("cameraSelected", self.camera_selected)
|
|
34
|
+
self.api.device_manager.on("cameraDeselected", self.camera_deselected)
|
|
35
|
+
|
|
36
|
+
async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
|
|
37
|
+
self.logger.log(f"New camera added ({extension}): {camera.name}")
|
|
38
|
+
self.cameras[camera.id] = camera
|
|
39
|
+
self.create_detector(camera)
|
|
40
|
+
|
|
41
|
+
def camera_deselected(self, id: str, extension: CameraExtension) -> None:
|
|
42
|
+
camera = self.cameras.get(id)
|
|
43
|
+
|
|
44
|
+
if camera:
|
|
45
|
+
self.logger.log(f"Camera removed ({extension}): {camera.name}")
|
|
46
|
+
self.remove_detector(self.cameras[id])
|
|
47
|
+
del self.cameras[id]
|
|
48
|
+
|
|
49
|
+
def start(self) -> None:
|
|
50
|
+
self.logger.log("Plugin Started")
|
|
51
|
+
|
|
52
|
+
for camera_device in self.cameras.values():
|
|
53
|
+
self.create_detector(camera_device)
|
|
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)
|
|
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
|
+
def create_detector(self, camera: CameraDevice) -> None:
|
|
71
|
+
detector = CameraDetector(self.api, camera, self.logger)
|
|
72
|
+
self.detectors[camera.id] = detector
|
|
73
|
+
|
|
74
|
+
def remove_detector(self, camera: CameraDevice) -> None:
|
|
75
|
+
detector = self.detectors[camera.id]
|
|
76
|
+
|
|
77
|
+
if detector:
|
|
78
|
+
detector.close()
|
|
79
|
+
del self.detectors[camera.id]
|
|
80
|
+
|
|
81
|
+
def interfaceSchema(self) -> RootSchema:
|
|
82
|
+
root_schema: RootSchema = {
|
|
83
|
+
"schema": {
|
|
84
|
+
"area": {
|
|
85
|
+
"type": "number",
|
|
86
|
+
"key": "area",
|
|
87
|
+
"title": "Area",
|
|
88
|
+
"description": "Minimum size of detected motion (pixels)",
|
|
89
|
+
"store": False,
|
|
90
|
+
"defaultValue": default_area,
|
|
91
|
+
"minimum": 10,
|
|
92
|
+
"maximum": 1000,
|
|
93
|
+
"step": 1,
|
|
94
|
+
"required": True,
|
|
95
|
+
},
|
|
96
|
+
"threshold": {
|
|
97
|
+
"type": "number",
|
|
98
|
+
"key": "threshold",
|
|
99
|
+
"title": "Threshold",
|
|
100
|
+
"description": "Sensitivity of motion detection (higher = less sensitive)",
|
|
101
|
+
"store": False,
|
|
102
|
+
"defaultValue": default_threshold,
|
|
103
|
+
"minimum": 0,
|
|
104
|
+
"maximum": 1,
|
|
105
|
+
"step": 0.01,
|
|
106
|
+
"required": True,
|
|
107
|
+
},
|
|
108
|
+
"blur": {
|
|
109
|
+
"type": "number",
|
|
110
|
+
"key": "blur",
|
|
111
|
+
"title": "Blur",
|
|
112
|
+
"description": "Gaussian blur radius to reduce noise",
|
|
113
|
+
"store": False,
|
|
114
|
+
"defaultValue": default_blur,
|
|
115
|
+
"minimum": 1,
|
|
116
|
+
"maximum": 21,
|
|
117
|
+
"step": 1,
|
|
118
|
+
"required": True,
|
|
119
|
+
},
|
|
120
|
+
"dilation": {
|
|
121
|
+
"type": "number",
|
|
122
|
+
"key": "dilation",
|
|
123
|
+
"title": "Dilation",
|
|
124
|
+
"description": "Expansion of detected motion areas",
|
|
125
|
+
"store": False,
|
|
126
|
+
"defaultValue": default_dilt,
|
|
127
|
+
"minimum": 1,
|
|
128
|
+
"maximum": 21,
|
|
129
|
+
"step": 1,
|
|
130
|
+
"required": True,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return root_schema
|
|
136
|
+
|
|
137
|
+
async def detectMotion(self, video_path: str, config: dict[str, Any]) -> MotionDetectionPluginResponse:
|
|
138
|
+
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
|
|
139
|
+
output_file = temp_file.name
|
|
140
|
+
|
|
141
|
+
cap = cv2.VideoCapture(video_path)
|
|
142
|
+
executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
|
|
143
|
+
|
|
144
|
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
145
|
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
146
|
+
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
|
147
|
+
|
|
148
|
+
fourcc = cv2.VideoWriter.fourcc("a", "v", "c", "1")
|
|
149
|
+
out = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
|
|
150
|
+
|
|
151
|
+
opencl_detector: Optional[OpenCLMotionDetector] = None
|
|
152
|
+
|
|
153
|
+
while cap.isOpened():
|
|
154
|
+
ret, frame = cap.read()
|
|
155
|
+
if not ret:
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
159
|
+
blur = config.get("blur", default_blur)
|
|
160
|
+
threshold = config.get("threshold", default_threshold)
|
|
161
|
+
area = config.get("area", default_area)
|
|
162
|
+
dilation = config.get("dilation", default_dilt)
|
|
163
|
+
|
|
164
|
+
if opencl_detector is None:
|
|
165
|
+
opencl_detector = OpenCLMotionDetector(self.logger, width, height, blur)
|
|
166
|
+
|
|
167
|
+
dets = await asyncio.get_event_loop().run_in_executor(
|
|
168
|
+
executor,
|
|
169
|
+
opencl_detector.process_frame,
|
|
170
|
+
gray,
|
|
171
|
+
threshold,
|
|
172
|
+
dilation,
|
|
173
|
+
area,
|
|
174
|
+
default_alpha,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
merged_boxed = merge_overlapping_boxes(dets)
|
|
178
|
+
|
|
179
|
+
for det in merged_boxed:
|
|
180
|
+
(x1, y1, x2, y2) = det
|
|
181
|
+
|
|
182
|
+
pt1: cv2.typing.Point = (int(x1), int(y1))
|
|
183
|
+
pt2: cv2.typing.Point = (int(x2), int(y2))
|
|
184
|
+
|
|
185
|
+
cv2.rectangle(frame, pt1, pt2, (0, 255, 0), 2)
|
|
186
|
+
|
|
187
|
+
out.write(frame)
|
|
188
|
+
|
|
189
|
+
cap.release()
|
|
190
|
+
out.release()
|
|
191
|
+
executor.shutdown()
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"filePath": output_file,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def __main__():
|
|
199
|
+
return OpenCL
|
package/src/opencl.cl
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
|
|
2
|
+
|
|
3
|
+
__kernel void gaussian_blur_horizontal(
|
|
4
|
+
__read_only image2d_t input,
|
|
5
|
+
__write_only image2d_t output,
|
|
6
|
+
__global float *kernel_values,
|
|
7
|
+
int kernel_size
|
|
8
|
+
) {
|
|
9
|
+
int x = get_global_id(0);
|
|
10
|
+
int y = get_global_id(1);
|
|
11
|
+
|
|
12
|
+
int width = get_image_width(input);
|
|
13
|
+
int radius = (kernel_size - 1) / 2;
|
|
14
|
+
|
|
15
|
+
if (x >= width) return;
|
|
16
|
+
|
|
17
|
+
float sum = 0.0f;
|
|
18
|
+
for (int k = -radius; k <= radius; k++) {
|
|
19
|
+
int sx = clamp(x + k, 0, width - 1);
|
|
20
|
+
float4 pixel = read_imagef(input, sampler, (int2)(sx, y));
|
|
21
|
+
float weight = kernel_values[k + radius];
|
|
22
|
+
sum += pixel.x * weight;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
write_imagef(output, (int2)(x, y), (float4)(sum, 0.0f, 0.0f, 1.0f));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
__kernel void gaussian_blur_vertical(
|
|
29
|
+
__read_only image2d_t input,
|
|
30
|
+
__write_only image2d_t output,
|
|
31
|
+
__global float *kernel_values,
|
|
32
|
+
int kernel_size
|
|
33
|
+
) {
|
|
34
|
+
int x = get_global_id(0);
|
|
35
|
+
int y = get_global_id(1);
|
|
36
|
+
|
|
37
|
+
int height = get_image_height(input);
|
|
38
|
+
int radius = (kernel_size - 1) / 2;
|
|
39
|
+
|
|
40
|
+
if (y >= height) return;
|
|
41
|
+
|
|
42
|
+
float sum = 0.0f;
|
|
43
|
+
for (int k = -radius; k <= radius; k++) {
|
|
44
|
+
int sy = clamp(y + k, 0, height - 1);
|
|
45
|
+
float4 pixel = read_imagef(input, sampler, (int2)(x, sy));
|
|
46
|
+
float weight = kernel_values[k + radius];
|
|
47
|
+
sum += pixel.x * weight;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
write_imagef(output, (int2)(x, y), (float4)(sum, 0.0f, 0.0f, 1.0f));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
__kernel void background_subtraction(
|
|
54
|
+
__read_only image2d_t current_frame,
|
|
55
|
+
__global float *background_model,
|
|
56
|
+
__write_only image2d_t mask,
|
|
57
|
+
int width,
|
|
58
|
+
int height,
|
|
59
|
+
float alpha,
|
|
60
|
+
float threshold
|
|
61
|
+
) {
|
|
62
|
+
int x = get_global_id(0);
|
|
63
|
+
int y = get_global_id(1);
|
|
64
|
+
if (x >= width || y >= height) return;
|
|
65
|
+
int idx = y * width + x;
|
|
66
|
+
|
|
67
|
+
float current_pixel = read_imagef(current_frame, sampler, (int2)(x, y)).x;
|
|
68
|
+
float bg_pixel = background_model[idx];
|
|
69
|
+
|
|
70
|
+
// Aktualisiere Hintergrundmodell
|
|
71
|
+
float updated_bg = bg_pixel * (1.0f - alpha) + current_pixel * alpha;
|
|
72
|
+
background_model[idx] = updated_bg;
|
|
73
|
+
|
|
74
|
+
// Berechne Differenz
|
|
75
|
+
float diff = fabs(current_pixel - bg_pixel);
|
|
76
|
+
float result = (diff > threshold) ? 1.0f : 0.0f;
|
|
77
|
+
write_imagef(mask, (int2)(x, y), (float4)(result, 0.0f, 0.0f, 1.0f));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
__kernel void dilate(
|
|
81
|
+
__read_only image2d_t input,
|
|
82
|
+
__write_only image2d_t output,
|
|
83
|
+
int size
|
|
84
|
+
) {
|
|
85
|
+
int x = get_global_id(0);
|
|
86
|
+
int y = get_global_id(1);
|
|
87
|
+
int width = get_image_width(input);
|
|
88
|
+
int height = get_image_height(input);
|
|
89
|
+
if (x >= width || y >= height) return;
|
|
90
|
+
|
|
91
|
+
float maxValue = 0.0f;
|
|
92
|
+
for (int dy = -size; dy <= size; dy++) {
|
|
93
|
+
int sy = clamp(y + dy, 0, height - 1);
|
|
94
|
+
for (int dx = -size; dx <= size; dx++) {
|
|
95
|
+
int sx = clamp(x + dx, 0, width - 1);
|
|
96
|
+
float pixel = read_imagef(input, sampler, (int2)(sx, sy)).x;
|
|
97
|
+
maxValue = fmax(maxValue, pixel);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
write_imagef(output, (int2)(x, y), (float4)(maxValue, 0.0f, 0.0f, 1.0f));
|
|
101
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
from opencl_utils import get_contour_detections
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenCLMotionDetector:
|
|
11
|
+
def __init__(self, logger: LoggerService, width: int, height: int, blur_radius: int):
|
|
12
|
+
ocl = self.createContext()
|
|
13
|
+
|
|
14
|
+
if ocl is None:
|
|
15
|
+
raise RuntimeError("Failed to create OpenCL context.")
|
|
16
|
+
|
|
17
|
+
self.logger = logger
|
|
18
|
+
self.ctx: Any = ocl[0]
|
|
19
|
+
self.device: Any = ocl[1]
|
|
20
|
+
self.queue: Any = cl.CommandQueue(self.ctx, properties=cl.command_queue_properties.PROFILING_ENABLE)
|
|
21
|
+
self.program: Any = self.createProgram(self.ctx, self.device, "opencl.cl")
|
|
22
|
+
|
|
23
|
+
self.logger.debug(f"Device OpenCL version: {self.device.version}")
|
|
24
|
+
self.logger.debug(f"Selected device: {self.device.name}")
|
|
25
|
+
|
|
26
|
+
self.width = width
|
|
27
|
+
self.height = height
|
|
28
|
+
|
|
29
|
+
mf: Any = cl.mem_flags
|
|
30
|
+
|
|
31
|
+
# Erstelle die Images und Puffer
|
|
32
|
+
self.gray_image: Any = cl.Image(
|
|
33
|
+
self.ctx,
|
|
34
|
+
mf.READ_ONLY,
|
|
35
|
+
format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.UNORM_INT8),
|
|
36
|
+
shape=(self.width, self.height),
|
|
37
|
+
)
|
|
38
|
+
self.temp_image: Any = cl.Image(
|
|
39
|
+
self.ctx,
|
|
40
|
+
mf.READ_WRITE,
|
|
41
|
+
format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
42
|
+
shape=(self.width, self.height),
|
|
43
|
+
)
|
|
44
|
+
self.blurred_image: Any = cl.Image(
|
|
45
|
+
self.ctx,
|
|
46
|
+
mf.READ_WRITE,
|
|
47
|
+
format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
48
|
+
shape=(self.width, self.height),
|
|
49
|
+
)
|
|
50
|
+
self.mask_image: Any = cl.Image(
|
|
51
|
+
self.ctx,
|
|
52
|
+
mf.READ_WRITE,
|
|
53
|
+
format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
54
|
+
shape=(self.width, self.height),
|
|
55
|
+
)
|
|
56
|
+
self.dilated_image: Any = cl.Image(
|
|
57
|
+
self.ctx,
|
|
58
|
+
mf.WRITE_ONLY,
|
|
59
|
+
format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
60
|
+
shape=(self.width, self.height),
|
|
61
|
+
)
|
|
62
|
+
self.background_model_buf: Any = cl.Buffer(
|
|
63
|
+
self.ctx,
|
|
64
|
+
mf.READ_WRITE,
|
|
65
|
+
size=self.width * self.height * np.dtype(np.float32).itemsize,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Erstelle den Gaussian-Kernel
|
|
69
|
+
self.kernel_radius = blur_radius
|
|
70
|
+
self.kernel = self.create_gaussian_kernel(self.kernel_radius)
|
|
71
|
+
self.kernel_buf: Any = cl.Buffer(self.ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=self.kernel)
|
|
72
|
+
self.kernel_size = np.int32(len(self.kernel))
|
|
73
|
+
|
|
74
|
+
self.first_frame = True
|
|
75
|
+
|
|
76
|
+
def process_frame(
|
|
77
|
+
self,
|
|
78
|
+
gray_frame: np.ndarray[Any, Any],
|
|
79
|
+
motion_threshold: float,
|
|
80
|
+
dilation_size: int,
|
|
81
|
+
area_threshold: int,
|
|
82
|
+
alpha: float,
|
|
83
|
+
) -> list[tuple[float, float, float, float]]:
|
|
84
|
+
cl.enqueue_copy(
|
|
85
|
+
self.queue,
|
|
86
|
+
self.gray_image,
|
|
87
|
+
gray_frame,
|
|
88
|
+
origin=(0, 0),
|
|
89
|
+
region=(self.width, self.height),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
global_size = (self.width, self.height)
|
|
93
|
+
|
|
94
|
+
self.program.gaussian_blur_horizontal(
|
|
95
|
+
self.queue,
|
|
96
|
+
global_size,
|
|
97
|
+
None,
|
|
98
|
+
self.gray_image,
|
|
99
|
+
self.temp_image,
|
|
100
|
+
self.kernel_buf,
|
|
101
|
+
self.kernel_size,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.program.gaussian_blur_vertical(
|
|
105
|
+
self.queue,
|
|
106
|
+
global_size,
|
|
107
|
+
None,
|
|
108
|
+
self.temp_image,
|
|
109
|
+
self.blurred_image,
|
|
110
|
+
self.kernel_buf,
|
|
111
|
+
self.kernel_size,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not self.first_frame:
|
|
115
|
+
self.program.background_subtraction(
|
|
116
|
+
self.queue,
|
|
117
|
+
global_size,
|
|
118
|
+
None,
|
|
119
|
+
self.blurred_image,
|
|
120
|
+
self.background_model_buf,
|
|
121
|
+
self.mask_image,
|
|
122
|
+
np.int32(self.width),
|
|
123
|
+
np.int32(self.height),
|
|
124
|
+
np.float32(alpha),
|
|
125
|
+
np.float32(motion_threshold),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.program.dilate(
|
|
129
|
+
self.queue,
|
|
130
|
+
global_size,
|
|
131
|
+
None,
|
|
132
|
+
self.mask_image,
|
|
133
|
+
self.dilated_image,
|
|
134
|
+
np.int32(dilation_size),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
motion_mask = np.empty((self.height, self.width), dtype=np.float32)
|
|
138
|
+
cl.enqueue_copy(
|
|
139
|
+
self.queue,
|
|
140
|
+
motion_mask,
|
|
141
|
+
self.dilated_image,
|
|
142
|
+
origin=(0, 0),
|
|
143
|
+
region=(self.width, self.height),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return get_contour_detections((motion_mask * 255).astype(np.uint8), area_threshold)
|
|
147
|
+
else:
|
|
148
|
+
background_model_np = np.empty((self.height, self.width), dtype=np.float32)
|
|
149
|
+
cl.enqueue_copy(
|
|
150
|
+
self.queue,
|
|
151
|
+
background_model_np,
|
|
152
|
+
self.blurred_image,
|
|
153
|
+
origin=(0, 0),
|
|
154
|
+
region=(self.width, self.height),
|
|
155
|
+
)
|
|
156
|
+
background_model_np = background_model_np.flatten()
|
|
157
|
+
cl.enqueue_copy(self.queue, self.background_model_buf, background_model_np)
|
|
158
|
+
self.first_frame = False
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
def create_gaussian_kernel(self, radius: int) -> np.ndarray[Any, Any]:
|
|
162
|
+
sigma = radius / 3.0
|
|
163
|
+
x = np.arange(-radius, radius + 1)
|
|
164
|
+
kernel = np.exp(-(x**2) / (2 * sigma**2))
|
|
165
|
+
kernel = kernel / np.sum(kernel)
|
|
166
|
+
return kernel.astype(np.float32)
|
|
167
|
+
|
|
168
|
+
def createContext(self) -> tuple[Any, Any] | None:
|
|
169
|
+
platforms: Any = cl.get_platforms()
|
|
170
|
+
if len(platforms) == 0:
|
|
171
|
+
self.logger.warn("Failed to find any OpenCL platforms.")
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
devices = platforms[0].get_devices(cl.device_type.GPU)
|
|
175
|
+
if len(devices) == 0:
|
|
176
|
+
self.logger.warn("Could not find GPU device, trying CPU...")
|
|
177
|
+
devices = platforms[0].get_devices(cl.device_type.CPU)
|
|
178
|
+
if len(devices) == 0:
|
|
179
|
+
self.logger.warn("Could not find OpenCL GPU or CPU device.")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
context: Any = cl.Context([devices[0]])
|
|
183
|
+
return context, devices[0]
|
|
184
|
+
|
|
185
|
+
def createProgram(self, context: Any, device: Any, filename: str) -> Any:
|
|
186
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
187
|
+
full_path = os.path.join(current_dir, filename)
|
|
188
|
+
|
|
189
|
+
with open(full_path) as kernelFile:
|
|
190
|
+
kernelStr = kernelFile.read()
|
|
191
|
+
|
|
192
|
+
program = cl.Program(context, kernelStr)
|
|
193
|
+
program.build(devices=[device])
|
|
194
|
+
|
|
195
|
+
return program
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
DEFAULT_AREA_THRESHOLD = 500
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_contour_detections(
|
|
10
|
+
mask: cv2.typing.MatLike, area_threshold: int = DEFAULT_AREA_THRESHOLD
|
|
11
|
+
) -> list[tuple[float, float, float, float]]:
|
|
12
|
+
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
13
|
+
detections: list[tuple[float, float, float, float]] = []
|
|
14
|
+
for cnt in cnts:
|
|
15
|
+
x, y, w, h = cv2.boundingRect(cnt)
|
|
16
|
+
area = w * h
|
|
17
|
+
if area > area_threshold:
|
|
18
|
+
detections.append((x, y, x + w, y + h))
|
|
19
|
+
return detections
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def merge_overlapping_boxes(
|
|
23
|
+
boxes: list[tuple[float, float, float, float]], overlap_threshold: float = 0.1
|
|
24
|
+
) -> list[tuple[float, float, float, float]]:
|
|
25
|
+
if not boxes:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
boxes_array = np.array(boxes)
|
|
29
|
+
merged_boxes: list[tuple[float, float, float, float]] = []
|
|
30
|
+
|
|
31
|
+
while len(boxes_array) > 0:
|
|
32
|
+
box = boxes_array[0]
|
|
33
|
+
boxes_array: np.ndarray[Any, Any] = boxes_array[1:]
|
|
34
|
+
|
|
35
|
+
x1, y1, w1, h1 = box
|
|
36
|
+
x2 = x1 + w1
|
|
37
|
+
y2 = y1 + h1
|
|
38
|
+
|
|
39
|
+
indices_to_remove: list[int] = []
|
|
40
|
+
for i, (x1o, y1o, w1o, h1o) in enumerate(boxes_array):
|
|
41
|
+
x2o = x1o + w1o
|
|
42
|
+
y2o = y1o + h1o
|
|
43
|
+
|
|
44
|
+
xx1 = max(x1, x1o)
|
|
45
|
+
yy1 = max(y1, y1o)
|
|
46
|
+
xx2 = min(x2, x2o)
|
|
47
|
+
yy2 = min(y2, y2o)
|
|
48
|
+
|
|
49
|
+
w_overlap = max(0, xx2 - xx1)
|
|
50
|
+
h_overlap = max(0, yy2 - yy1)
|
|
51
|
+
overlap_area = w_overlap * h_overlap
|
|
52
|
+
area1 = w1 * h1
|
|
53
|
+
area2 = w1o * h1o
|
|
54
|
+
|
|
55
|
+
if area1 == 0 or area2 == 0:
|
|
56
|
+
continue
|
|
57
|
+
overlap_ratio = overlap_area / min(area1, area2)
|
|
58
|
+
|
|
59
|
+
if overlap_ratio > overlap_threshold:
|
|
60
|
+
x1 = min(x1, x1o)
|
|
61
|
+
y1 = min(y1, y1o)
|
|
62
|
+
x2 = max(x2, x2o)
|
|
63
|
+
y2 = max(y2, y2o)
|
|
64
|
+
indices_to_remove.append(i)
|
|
65
|
+
|
|
66
|
+
boxes_array = np.delete(boxes_array, indices_to_remove, axis=0)
|
|
67
|
+
merged_boxes.append((int(x1), int(y1), int(x2 - x1), int(y2 - y1)))
|
|
68
|
+
|
|
69
|
+
return merged_boxes
|