@camera.ui/camera-ui-opencl 0.0.3 → 0.0.4
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 +6 -3
- package/requirements.txt +1 -1
- package/src/camera_detector.py +132 -100
- package/src/main.py +19 -18
- package/src/opencl.cl +54 -85
- package/src/opencl_detector.py +119 -132
- package/src/opencl_utils.py +8 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "OpenCL",
|
|
3
3
|
"name": "@camera.ui/camera-ui-opencl",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"description": "camera.ui opencl 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.34-alpha.
|
|
24
|
+
"camera.ui": ">=0.0.34-alpha.2",
|
|
25
25
|
"node": ">=20.17.0"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/seydx/camera.ui#readme",
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"camera.ui": {
|
|
40
40
|
"pythonVersion": "3.11",
|
|
41
41
|
"extension": "motionDetection",
|
|
42
|
-
"dependencies": []
|
|
42
|
+
"dependencies": [],
|
|
43
|
+
"options": {
|
|
44
|
+
"extendedMotionDetection": true
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
}
|
package/requirements.txt
CHANGED
package/src/camera_detector.py
CHANGED
|
@@ -7,143 +7,168 @@ from camera_ui_python_types import (
|
|
|
7
7
|
CameraDevice,
|
|
8
8
|
CameraStorage,
|
|
9
9
|
Detection,
|
|
10
|
-
LoggerService,
|
|
11
10
|
PluginAPI,
|
|
12
11
|
)
|
|
12
|
+
|
|
13
13
|
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
14
|
-
from opencl_detector import OpenCLMotionDetector
|
|
14
|
+
from opencl_detector import OpenCLMotionDetector, createProgram
|
|
15
15
|
from plugin_typings import CameraStorageValues
|
|
16
16
|
|
|
17
|
-
background_tasks = set[asyncio.Task[Any]]()
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
class CameraDetector:
|
|
21
|
-
def __init__(self, api: PluginAPI,
|
|
19
|
+
def __init__(self, api: PluginAPI, camera_device: CameraDevice) -> None:
|
|
22
20
|
super().__init__()
|
|
23
21
|
|
|
24
22
|
self.api = api
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
23
|
+
self.camera_device = camera_device
|
|
24
|
+
self.camera_logger = camera_device.logger
|
|
27
25
|
|
|
28
26
|
self.started = False
|
|
29
27
|
self.closed = False
|
|
30
|
-
self.
|
|
28
|
+
self.restarting = False
|
|
29
|
+
|
|
30
|
+
self.initialized = False
|
|
31
|
+
|
|
31
32
|
self.executor = None
|
|
32
33
|
self.previous_frame: Optional[np.ndarray[Any, Any]] = None
|
|
33
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
|
+
|
|
34
41
|
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
42
|
|
|
37
|
-
def
|
|
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:
|
|
38
58
|
if not self.started:
|
|
39
59
|
self.started = True
|
|
40
60
|
self.closed = False
|
|
41
61
|
|
|
42
62
|
self.executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="OpenCV")
|
|
43
63
|
|
|
44
|
-
self.
|
|
45
|
-
|
|
46
|
-
self.frame_generation_task = asyncio.create_task(
|
|
47
|
-
self.__detect(), name=f"MotionDetection-{self.camera.name}"
|
|
48
|
-
)
|
|
64
|
+
self.camera_logger.log("Starting motion detection")
|
|
49
65
|
|
|
50
|
-
|
|
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
|
-
)
|
|
66
|
+
await self.__start_detection()
|
|
56
67
|
|
|
57
68
|
def close(self) -> None:
|
|
58
|
-
if
|
|
69
|
+
if not self.closed:
|
|
59
70
|
self.started = False
|
|
60
71
|
self.closed = True
|
|
61
72
|
|
|
62
|
-
self.
|
|
63
|
-
|
|
64
|
-
if self.frame_generation_task is not None:
|
|
65
|
-
self.frame_generation_task.cancel()
|
|
66
|
-
self.frame_generation_task = None
|
|
73
|
+
self.camera_logger.log("Stopping motion detection")
|
|
67
74
|
|
|
68
75
|
if self.executor is not None:
|
|
69
76
|
self.executor.shutdown(wait=False)
|
|
70
77
|
self.executor = None
|
|
71
78
|
|
|
72
|
-
def
|
|
73
|
-
|
|
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)
|
|
74
96
|
|
|
75
|
-
self.
|
|
97
|
+
self.restarting = False
|
|
76
98
|
|
|
77
|
-
if self.
|
|
78
|
-
self.
|
|
99
|
+
if self.camera_device.connected and not self.closed:
|
|
100
|
+
await self.__start_detection()
|
|
79
101
|
|
|
80
102
|
async def __detect(self) -> None:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
103
|
+
opencl_detector: Optional[OpenCLMotionDetector] = None
|
|
104
|
+
|
|
105
|
+
async for frame in self.camera_device.get_frames():
|
|
106
|
+
if self.closed:
|
|
135
107
|
break
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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)
|
|
141
166
|
|
|
142
167
|
def __create_camera_storage(self) -> CameraStorage[CameraStorageValues]:
|
|
143
168
|
camera_storage: CameraStorage[CameraStorageValues] = (
|
|
144
169
|
self.api.storage_controller.create_camera_storage(
|
|
145
170
|
self,
|
|
146
|
-
self.
|
|
171
|
+
self.camera_device.id,
|
|
147
172
|
{
|
|
148
173
|
"area": {
|
|
149
174
|
"type": "number",
|
|
@@ -156,8 +181,8 @@ class CameraDetector:
|
|
|
156
181
|
"maximum": 1000,
|
|
157
182
|
"step": 1,
|
|
158
183
|
"required": True,
|
|
159
|
-
"onSet": lambda
|
|
160
|
-
|
|
184
|
+
"onSet": lambda new, old: self.camera_logger.log(
|
|
185
|
+
f"OpenCL model motion area changed from {old} to {new}"
|
|
161
186
|
),
|
|
162
187
|
},
|
|
163
188
|
"threshold": {
|
|
@@ -171,8 +196,8 @@ class CameraDetector:
|
|
|
171
196
|
"maximum": 1,
|
|
172
197
|
"step": 0.01,
|
|
173
198
|
"required": True,
|
|
174
|
-
"onSet": lambda
|
|
175
|
-
|
|
199
|
+
"onSet": lambda new, old: self.camera_logger.log(
|
|
200
|
+
f"OpenCL model threshold changed from {old} to {new}"
|
|
176
201
|
),
|
|
177
202
|
},
|
|
178
203
|
"blur": {
|
|
@@ -186,8 +211,8 @@ class CameraDetector:
|
|
|
186
211
|
"maximum": 21,
|
|
187
212
|
"step": 1,
|
|
188
213
|
"required": True,
|
|
189
|
-
"onSet": lambda
|
|
190
|
-
|
|
214
|
+
"onSet": lambda new, old: self.camera_logger.log(
|
|
215
|
+
f"OpenCL model blur radius changed from {old} to {new}"
|
|
191
216
|
),
|
|
192
217
|
},
|
|
193
218
|
"dilation": {
|
|
@@ -201,11 +226,18 @@ class CameraDetector:
|
|
|
201
226
|
"maximum": 21,
|
|
202
227
|
"step": 1,
|
|
203
228
|
"required": True,
|
|
204
|
-
"onSet": lambda
|
|
205
|
-
self.
|
|
229
|
+
"onSet": lambda new, old: self.camera_logger.log(
|
|
230
|
+
self.camera_device.name,
|
|
206
231
|
f"OpenCL model dilation size changed from {old} to {new}",
|
|
207
232
|
),
|
|
208
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
|
+
},
|
|
209
241
|
},
|
|
210
242
|
)
|
|
211
243
|
)
|
package/src/main.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import tempfile
|
|
3
3
|
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Union
|
|
5
5
|
|
|
6
6
|
import cv2
|
|
7
|
-
from camera_detector import CameraDetector
|
|
8
7
|
from camera_ui_python_types import (
|
|
9
8
|
CameraDevice,
|
|
10
9
|
CameraExtension,
|
|
@@ -15,8 +14,10 @@ from camera_ui_python_types import (
|
|
|
15
14
|
PluginAPI,
|
|
16
15
|
RootSchema,
|
|
17
16
|
)
|
|
17
|
+
|
|
18
|
+
from camera_detector import CameraDetector
|
|
18
19
|
from detector_defaults import default_alpha, default_area, default_blur, default_dilt, default_threshold
|
|
19
|
-
from opencl_detector import OpenCLMotionDetector
|
|
20
|
+
from opencl_detector import OpenCLMotionDetector, createProgram
|
|
20
21
|
from opencl_utils import merge_overlapping_boxes
|
|
21
22
|
|
|
22
23
|
|
|
@@ -36,7 +37,7 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
36
37
|
async def camera_selected(self, camera: CameraDevice, extension: CameraExtension) -> None:
|
|
37
38
|
self.logger.log(f"New camera added ({extension}): {camera.name}")
|
|
38
39
|
self.cameras[camera.id] = camera
|
|
39
|
-
self.create_detector(camera)
|
|
40
|
+
await self.create_detector(camera)
|
|
40
41
|
|
|
41
42
|
def camera_deselected(self, id: str, extension: CameraExtension) -> None:
|
|
42
43
|
camera = self.cameras.get(id)
|
|
@@ -46,11 +47,10 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
46
47
|
self.remove_detector(self.cameras[id])
|
|
47
48
|
del self.cameras[id]
|
|
48
49
|
|
|
49
|
-
def start(self) -> None:
|
|
50
|
+
async def start(self) -> None:
|
|
50
51
|
self.logger.log("Plugin Started")
|
|
51
52
|
|
|
52
|
-
for
|
|
53
|
-
self.create_detector(camera_device)
|
|
53
|
+
await asyncio.gather(*[self.create_detector(camera) for camera in self.cameras.values()])
|
|
54
54
|
|
|
55
55
|
def stop(self) -> None:
|
|
56
56
|
self.logger.log("Plugin Stopped")
|
|
@@ -58,7 +58,7 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
58
58
|
detectors_copy = list(self.detectors.values())
|
|
59
59
|
|
|
60
60
|
for detector in detectors_copy:
|
|
61
|
-
self.remove_detector(detector.
|
|
61
|
+
self.remove_detector(detector.camera_device)
|
|
62
62
|
|
|
63
63
|
async def onFormSubmit(self, action_id: str, payload: Any) -> Union[FormSubmitResponse, None]:
|
|
64
64
|
print("Form submitted", action_id, payload)
|
|
@@ -67,9 +67,10 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
67
67
|
for camera in cameras:
|
|
68
68
|
self.cameras[camera.id] = camera
|
|
69
69
|
|
|
70
|
-
def create_detector(self, camera: CameraDevice) -> None:
|
|
71
|
-
detector = CameraDetector(self.api, camera
|
|
70
|
+
async def create_detector(self, camera: CameraDevice) -> None:
|
|
71
|
+
detector = CameraDetector(self.api, camera)
|
|
72
72
|
self.detectors[camera.id] = detector
|
|
73
|
+
await detector.initialize()
|
|
73
74
|
|
|
74
75
|
def remove_detector(self, camera: CameraDevice) -> None:
|
|
75
76
|
detector = self.detectors[camera.id]
|
|
@@ -135,6 +136,8 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
135
136
|
return root_schema
|
|
136
137
|
|
|
137
138
|
async def detectMotion(self, video_path: str, config: dict[str, Any]) -> MotionDetectionPluginResponse:
|
|
139
|
+
ctx = createProgram()
|
|
140
|
+
|
|
138
141
|
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
|
|
139
142
|
output_file = temp_file.name
|
|
140
143
|
|
|
@@ -148,7 +151,12 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
148
151
|
fourcc = cv2.VideoWriter.fourcc("a", "v", "c", "1")
|
|
149
152
|
out = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
|
|
150
153
|
|
|
151
|
-
|
|
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)
|
|
152
160
|
|
|
153
161
|
while cap.isOpened():
|
|
154
162
|
ret, frame = cap.read()
|
|
@@ -156,13 +164,6 @@ class OpenCL(MotionDetectionPlugin):
|
|
|
156
164
|
break
|
|
157
165
|
|
|
158
166
|
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
|
|
|
167
168
|
dets = await asyncio.get_event_loop().run_in_executor(
|
|
168
169
|
executor,
|
package/src/opencl.cl
CHANGED
|
@@ -1,101 +1,70 @@
|
|
|
1
1
|
__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
|
|
2
2
|
|
|
3
|
-
__kernel void
|
|
3
|
+
__kernel void process_frame(
|
|
4
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
5
|
__global float *background_model,
|
|
56
|
-
|
|
6
|
+
__global float *temp_buffer,
|
|
7
|
+
__write_only image2d_t output,
|
|
8
|
+
__constant float *kernel_values,
|
|
9
|
+
int kernel_size,
|
|
57
10
|
int width,
|
|
58
11
|
int height,
|
|
59
12
|
float alpha,
|
|
60
|
-
float threshold
|
|
13
|
+
float threshold,
|
|
14
|
+
int dilation_size,
|
|
15
|
+
int first_frame
|
|
61
16
|
) {
|
|
62
17
|
int x = get_global_id(0);
|
|
63
18
|
int y = get_global_id(1);
|
|
64
19
|
if (x >= width || y >= height) return;
|
|
65
20
|
int idx = y * width + x;
|
|
66
21
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
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
|
+
}
|
|
90
33
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
66
|
}
|
|
67
|
+
|
|
68
|
+
write_imagef(output, (int2)(x, y), (float4)(maxValue, maxValue, maxValue, 1.0f));
|
|
99
69
|
}
|
|
100
|
-
|
|
101
|
-
}
|
|
70
|
+
}
|
package/src/opencl_detector.py
CHANGED
|
@@ -4,75 +4,106 @@ from typing import Any
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pyopencl as cl
|
|
6
6
|
from camera_ui_python_types import LoggerService
|
|
7
|
+
|
|
7
8
|
from opencl_utils import get_contour_detections
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ocl = self.createContext()
|
|
11
|
+
def createProgram() -> Any:
|
|
12
|
+
platforms: Any = cl.get_platforms()
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if len(platforms) == 0:
|
|
15
|
+
raise RuntimeError("Failed to find any OpenCL platforms.")
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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")
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
with open(full_path) as kernelFile:
|
|
30
|
+
kernelStr = kernelFile.read()
|
|
25
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
|
|
26
48
|
self.width = width
|
|
27
49
|
self.height = height
|
|
28
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
|
+
|
|
29
57
|
mf: Any = cl.mem_flags
|
|
30
58
|
|
|
31
|
-
#
|
|
32
|
-
self.
|
|
59
|
+
# Create reusable OpenCL images and buffers
|
|
60
|
+
self.input_image: Any = cl.Image(
|
|
33
61
|
self.ctx,
|
|
34
62
|
mf.READ_ONLY,
|
|
35
|
-
|
|
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),
|
|
63
|
+
cl.ImageFormat(cl.channel_order.R, cl.channel_type.UNORM_INT8),
|
|
54
64
|
shape=(self.width, self.height),
|
|
55
65
|
)
|
|
56
|
-
self.
|
|
66
|
+
self.result_image: Any = cl.Image(
|
|
57
67
|
self.ctx,
|
|
58
68
|
mf.WRITE_ONLY,
|
|
59
|
-
|
|
69
|
+
cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
|
|
60
70
|
shape=(self.width, self.height),
|
|
61
71
|
)
|
|
62
72
|
self.background_model_buf: Any = cl.Buffer(
|
|
63
|
-
self.ctx,
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
77
|
)
|
|
67
78
|
|
|
68
|
-
#
|
|
69
|
-
self.
|
|
70
|
-
|
|
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)
|
|
71
84
|
self.kernel_buf: Any = cl.Buffer(self.ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=self.kernel)
|
|
72
85
|
self.kernel_size = np.int32(len(self.kernel))
|
|
73
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
|
+
|
|
74
96
|
self.first_frame = True
|
|
75
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
|
+
|
|
76
107
|
def process_frame(
|
|
77
108
|
self,
|
|
78
109
|
gray_frame: np.ndarray[Any, Any],
|
|
@@ -81,115 +112,71 @@ class OpenCLMotionDetector:
|
|
|
81
112
|
area_threshold: int,
|
|
82
113
|
alpha: float,
|
|
83
114
|
) -> list[tuple[float, float, float, float]]:
|
|
115
|
+
# Copy the frame to OpenCL image
|
|
84
116
|
cl.enqueue_copy(
|
|
85
117
|
self.queue,
|
|
86
|
-
self.
|
|
118
|
+
self.input_image,
|
|
87
119
|
gray_frame,
|
|
88
120
|
origin=(0, 0),
|
|
89
121
|
region=(self.width, self.height),
|
|
90
122
|
)
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.program.gaussian_blur_horizontal(
|
|
124
|
+
# Process frame
|
|
125
|
+
event = self.program.process_frame(
|
|
95
126
|
self.queue,
|
|
96
|
-
global_size,
|
|
97
|
-
|
|
98
|
-
self.
|
|
99
|
-
self.
|
|
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,
|
|
100
133
|
self.kernel_buf,
|
|
101
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),
|
|
102
141
|
)
|
|
103
142
|
|
|
104
|
-
|
|
143
|
+
# Copy results back to host
|
|
144
|
+
cl.enqueue_copy(
|
|
105
145
|
self.queue,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
self.
|
|
110
|
-
|
|
111
|
-
self.kernel_size,
|
|
146
|
+
self.host_result_buffer,
|
|
147
|
+
self.result_image,
|
|
148
|
+
origin=(0, 0),
|
|
149
|
+
region=(self.width, self.height),
|
|
150
|
+
wait_for=[event],
|
|
112
151
|
)
|
|
113
152
|
|
|
114
|
-
|
|
115
|
-
|
|
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)
|
|
153
|
+
motion_mask_cv = (self.host_result_buffer * 255).astype(np.uint8)
|
|
154
|
+
detections = get_contour_detections(motion_mask_cv, area_threshold)
|
|
167
155
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if len(platforms) == 0:
|
|
171
|
-
self.logger.warn("Failed to find any OpenCL platforms.")
|
|
172
|
-
return None
|
|
156
|
+
self.first_frame = False
|
|
157
|
+
return detections
|
|
173
158
|
|
|
174
|
-
|
|
175
|
-
|
|
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]
|
|
159
|
+
def __get_optimal_work_group_size(self, max_group: int, compute_units: int) -> tuple[int, int]:
|
|
160
|
+
optimal_group_count = compute_units * 4
|
|
184
161
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)
|
|
188
171
|
|
|
189
|
-
|
|
190
|
-
kernelStr = kernelFile.read()
|
|
172
|
+
return (min(max_group, self.width), 1)
|
|
191
173
|
|
|
192
|
-
|
|
193
|
-
|
|
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)
|
|
194
179
|
|
|
195
|
-
|
|
180
|
+
def __del__(self):
|
|
181
|
+
# Clean up OpenCL resources
|
|
182
|
+
self.queue.finish()
|
package/src/opencl_utils.py
CHANGED
|
@@ -3,20 +3,17 @@ from typing import Any
|
|
|
3
3
|
import cv2
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
|
-
DEFAULT_AREA_THRESHOLD = 500
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
def get_contour_detections(
|
|
10
|
-
mask:
|
|
8
|
+
mask: np.ndarray[Any, Any], area_threshold: int
|
|
11
9
|
) -> list[tuple[float, float, float, float]]:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return detections
|
|
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
|
+
]
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
def merge_overlapping_boxes(
|