@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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ {
2
+ "schema": {}
3
+ }
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
+ }
@@ -0,0 +1,5 @@
1
+ numpy==1.26.4
2
+ opencv-python-headless==4.10.0.84
3
+ camera-ui-python-types==0.1.90
4
+ pyopencl==2024.2.7
5
+ siphash24==1.6
@@ -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
@@ -0,0 +1,5 @@
1
+ default_area = 400
2
+ default_threshold = 0.1
3
+ default_blur = 6
4
+ default_dilt = 6
5
+ default_alpha = 0.2
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
@@ -0,0 +1,8 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class CameraStorageValues(TypedDict):
5
+ area: int
6
+ threshold: int
7
+ blur: int
8
+ dilation: int