@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "OpenCL",
3
3
  "name": "@camera.ui/camera-ui-opencl",
4
- "version": "0.0.3",
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.1",
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
@@ -1,5 +1,5 @@
1
1
  numpy==1.26.4
2
2
  opencv-python-headless==4.10.0.84
3
- camera-ui-python-types==0.1.93
3
+ camera-ui-python-types==0.1.96
4
4
  pyopencl==2024.2.7
5
5
  siphash24==1.6
@@ -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, camera: CameraDevice, logger: LoggerService) -> None:
19
+ def __init__(self, api: PluginAPI, camera_device: CameraDevice) -> None:
22
20
  super().__init__()
23
21
 
24
22
  self.api = api
25
- self.camera = camera
26
- self.logger = logger
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.frame_generation_task: Optional[asyncio.Task[Any]] = None
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 start(self) -> None:
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.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
- )
64
+ self.camera_logger.log("Starting motion detection")
49
65
 
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
- )
66
+ await self.__start_detection()
56
67
 
57
68
  def close(self) -> None:
58
- if self.started and not self.closed:
69
+ if not self.closed:
59
70
  self.started = False
60
71
  self.closed = True
61
72
 
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
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 restart(self) -> None:
73
- self.logger.log(f"Restarting motion detection for camera {self.camera.name}...")
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.close()
97
+ self.restarting = False
76
98
 
77
- if self.camera.connected:
78
- self.start()
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
- 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:
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
- 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
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.camera.id,
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 old, new: self.logger.log(
160
- self.camera.name, f"OpenCL model motion area changed from {old} to {new}"
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 old, new: self.logger.log(
175
- self.camera.name, f"OpenCL model threshold changed from {old} to {new}"
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 old, new: self.logger.log(
190
- self.camera.name, f"OpenCL model blur radius changed from {old} to {new}"
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 old, new: self.logger.log(
205
- self.camera.name,
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, Optional, Union
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 camera_device in self.cameras.values():
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.camera)
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, self.logger)
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
- opencl_detector: Optional[OpenCLMotionDetector] = None
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 gaussian_blur_horizontal(
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
- __write_only image2d_t mask,
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
- 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;
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
- 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);
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
- write_imagef(output, (int2)(x, y), (float4)(maxValue, 0.0f, 0.0f, 1.0f));
101
- }
70
+ }
@@ -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
- class OpenCLMotionDetector:
11
- def __init__(self, logger: LoggerService, width: int, height: int, blur_radius: int):
12
- ocl = self.createContext()
11
+ def createProgram() -> Any:
12
+ platforms: Any = cl.get_platforms()
13
13
 
14
- if ocl is None:
15
- raise RuntimeError("Failed to create OpenCL context.")
14
+ if len(platforms) == 0:
15
+ raise RuntimeError("Failed to find any OpenCL platforms.")
16
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")
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
- self.logger.debug(f"Device OpenCL version: {self.device.version}")
24
- self.logger.debug(f"Selected device: {self.device.name}")
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
- # Erstelle die Images und Puffer
32
- self.gray_image: Any = cl.Image(
59
+ # Create reusable OpenCL images and buffers
60
+ self.input_image: Any = cl.Image(
33
61
  self.ctx,
34
62
  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),
63
+ cl.ImageFormat(cl.channel_order.R, cl.channel_type.UNORM_INT8),
54
64
  shape=(self.width, self.height),
55
65
  )
56
- self.dilated_image: Any = cl.Image(
66
+ self.result_image: Any = cl.Image(
57
67
  self.ctx,
58
68
  mf.WRITE_ONLY,
59
- format=cl.ImageFormat(cl.channel_order.R, cl.channel_type.FLOAT),
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
- mf.READ_WRITE,
65
- size=self.width * self.height * np.dtype(np.float32).itemsize,
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
- # Erstelle den Gaussian-Kernel
69
- self.kernel_radius = blur_radius
70
- self.kernel = self.create_gaussian_kernel(self.kernel_radius)
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.gray_image,
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
- global_size = (self.width, self.height)
93
-
94
- self.program.gaussian_blur_horizontal(
124
+ # Process frame
125
+ event = self.program.process_frame(
95
126
  self.queue,
96
- global_size,
97
- None,
98
- self.gray_image,
99
- self.temp_image,
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
- self.program.gaussian_blur_vertical(
143
+ # Copy results back to host
144
+ cl.enqueue_copy(
105
145
  self.queue,
106
- global_size,
107
- None,
108
- self.temp_image,
109
- self.blurred_image,
110
- self.kernel_buf,
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
- 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)
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
- 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
156
+ self.first_frame = False
157
+ return detections
173
158
 
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]
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
- 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)
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
- with open(full_path) as kernelFile:
190
- kernelStr = kernelFile.read()
172
+ return (min(max_group, self.width), 1)
191
173
 
192
- program = cl.Program(context, kernelStr)
193
- program.build(devices=[device])
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
- return program
180
+ def __del__(self):
181
+ # Clean up OpenCL resources
182
+ self.queue.finish()
@@ -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: cv2.typing.MatLike, area_threshold: int = DEFAULT_AREA_THRESHOLD
8
+ mask: np.ndarray[Any, Any], area_threshold: int
11
9
  ) -> 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
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(