valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b42__py3-none-any.whl

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.
@@ -4,53 +4,79 @@ Drawable is part of the Image_Handler
4
4
  used functions to draw the elements on the Numpy Array
5
5
  that is actually our camera frame.
6
6
  Version: v2024.12.0
7
+ Refactored for clarity, consistency, and optimized parameter usage.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
10
11
 
12
+ import asyncio
13
+ import logging
11
14
  import math
12
15
 
16
+ # cv2 is imported but not used directly in this file
17
+ # It's needed for other modules that import from here
13
18
  import numpy as np
14
19
  from PIL import ImageDraw, ImageFont
15
20
 
16
- from .types import Color, NumpyArray, PilPNG, Point, Union
21
+ from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
22
+
23
+
24
+ _LOGGER = logging.getLogger(__name__)
17
25
 
18
26
 
19
27
  class Drawable:
20
28
  """
21
29
  Collection of drawing utility functions for the image handlers.
22
- This class contains static methods to draw various elements on the Numpy Arrays (images).
23
- We cant use openCV because it is not supported by the Home Assistant OS.
30
+ This class contains static methods to draw various elements on NumPy arrays (images).
31
+ We can't use OpenCV because it is not supported by the Home Assistant OS.
24
32
  """
25
33
 
26
- ERROR_OUTLINE = (0, 0, 0, 255) # Red color for error messages
27
- ERROR_COLOR = (255, 0, 0, 191) # Red color with lower opacity for error outlines
34
+ ERROR_OUTLINE: Color = (0, 0, 0, 255) # Red color for error messages
35
+ ERROR_COLOR: Color = (
36
+ 255,
37
+ 0,
38
+ 0,
39
+ 191,
40
+ ) # Red color with lower opacity for error outlines
28
41
 
29
42
  @staticmethod
30
43
  async def create_empty_image(
31
44
  width: int, height: int, background_color: Color
32
45
  ) -> NumpyArray:
33
- """Create the empty background image numpy array.
34
- Background color is specified as RGBA tuple."""
35
- image_array = np.full((height, width, 4), background_color, dtype=np.uint8)
36
- return image_array
46
+ """Create the empty background image NumPy array.
47
+ Background color is specified as an RGBA tuple."""
48
+ return np.full((height, width, 4), background_color, dtype=np.uint8)
37
49
 
38
50
  @staticmethod
39
51
  async def from_json_to_image(
40
52
  layer: NumpyArray, pixels: Union[dict, list], pixel_size: int, color: Color
41
53
  ) -> NumpyArray:
42
- """Drawing the layers (rooms) from the vacuum json data."""
54
+ """Draw the layers (rooms) from the vacuum JSON data onto the image array."""
43
55
  image_array = layer
56
+ # Extract alpha from color
57
+ alpha = color[3] if len(color) == 4 else 255
58
+
59
+ # For debugging
60
+ _LOGGER.debug("Drawing with color %s and alpha %s", color, alpha)
61
+
62
+ # Create the full color with alpha
63
+ full_color = color if len(color) == 4 else (*color, 255)
64
+
44
65
  # Loop through pixels to find min and max coordinates
45
66
  for x, y, z in pixels:
46
67
  col = x * pixel_size
47
68
  row = y * pixel_size
48
- # Draw pixels
69
+ # Draw pixels as blocks
49
70
  for i in range(z):
50
- image_array[
71
+ # Get the region to update
72
+ region = image_array[
51
73
  row : row + pixel_size,
52
74
  col + i * pixel_size : col + (i + 1) * pixel_size,
53
- ] = color
75
+ ]
76
+
77
+ # Simple direct assignment - ignore alpha for now to ensure visibility
78
+ region[:] = full_color
79
+
54
80
  return image_array
55
81
 
56
82
  @staticmethod
@@ -60,12 +86,10 @@ class Drawable:
60
86
  """Draw the battery charger on the input layer."""
61
87
  charger_width = 10
62
88
  charger_height = 20
63
- # Get the starting and ending indices of the charger rectangle
64
89
  start_row = y - charger_height // 2
65
90
  end_row = start_row + charger_height
66
91
  start_col = x - charger_width // 2
67
92
  end_col = start_col + charger_width
68
- # Fill in the charger rectangle with the specified color
69
93
  layers[start_row:end_row, start_col:end_col] = color
70
94
  return layers
71
95
 
@@ -74,13 +98,10 @@ class Drawable:
74
98
  layer: NumpyArray, center: Point, rotation_angle: int, flag_color: Color
75
99
  ) -> NumpyArray:
76
100
  """
77
- It is draw a flag on centered at specified coordinates on
78
- the input layer. It uses the rotation angle of the image
79
- to orientate the flag on the given layer.
101
+ Draw a flag centered at specified coordinates on the input layer.
102
+ It uses the rotation angle of the image to orient the flag.
80
103
  """
81
- # Define flag color
82
- pole_color = (0, 0, 255, 255) # RGBA color (blue)
83
- # Define flag size and position
104
+ pole_color: Color = (0, 0, 255, 255) # Blue for the pole
84
105
  flag_size = 50
85
106
  pole_width = 6
86
107
  # Adjust flag coordinates based on rotation angle
@@ -91,23 +112,16 @@ class Drawable:
91
112
  y2 = y1 + (flag_size // 2)
92
113
  x3 = center[0] + (flag_size // 2)
93
114
  y3 = center[1] - (pole_width // 2)
94
- # Define pole end position
95
- xp1 = center[0]
96
- yp1 = center[1] - (pole_width // 2)
97
- xp2 = center[0] + flag_size
98
- yp2 = center[1] - (pole_width // 2)
115
+ xp1, yp1 = center[0], center[1] - (pole_width // 2)
116
+ xp2, yp2 = center[0] + flag_size, center[1] - (pole_width // 2)
99
117
  elif rotation_angle == 180:
100
118
  x1 = center[0]
101
119
  y1 = center[1] - (flag_size // 2)
102
120
  x2 = center[0] - (flag_size // 2)
103
121
  y2 = y1 + (flag_size // 4)
104
- x3 = center[0]
105
- y3 = center[1]
106
- # Define pole end position
107
- xp1 = center[0] + (pole_width // 2)
108
- yp1 = center[1] - flag_size
109
- xp2 = center[0] + (pole_width // 2)
110
- yp2 = y3
122
+ x3, y3 = center[0], center[1]
123
+ xp1, yp1 = center[0] + (pole_width // 2), center[1] - flag_size
124
+ xp2, yp2 = center[0] + (pole_width // 2), y3
111
125
  elif rotation_angle == 270:
112
126
  x1 = center[0] - flag_size
113
127
  y1 = center[1] + (pole_width // 2)
@@ -115,61 +129,40 @@ class Drawable:
115
129
  y2 = y1 - (flag_size // 2)
116
130
  x3 = center[0] - (flag_size // 2)
117
131
  y3 = center[1] + (pole_width // 2)
118
- # Define pole end position
119
- xp1 = center[0] - flag_size
120
- yp1 = center[1] + (pole_width // 2)
121
- xp2 = center[0]
122
- yp2 = center[1] + (pole_width // 2)
123
- else:
124
- # rotation_angle == 0 (no rotation)
125
- x1 = center[0]
126
- y1 = center[1]
127
- x2 = center[0] + (flag_size // 2)
128
- y2 = y1 + (flag_size // 4)
129
- x3 = center[0]
130
- y3 = center[1] + flag_size // 2
131
- # Define pole end position
132
- xp1 = center[0] - (pole_width // 2)
133
- yp1 = y1
134
- xp2 = center[0] - (pole_width // 2)
135
- yp2 = center[1] + flag_size
132
+ xp1, yp1 = center[0] - flag_size, center[1] + (pole_width // 2)
133
+ xp2, yp2 = center[0], center[1] + (pole_width // 2)
134
+ else: # rotation_angle == 0 (no rotation)
135
+ x1, y1 = center[0], center[1]
136
+ x2, y2 = center[0] + (flag_size // 2), center[1] + (flag_size // 4)
137
+ x3, y3 = center[0], center[1] + flag_size // 2
138
+ xp1, yp1 = center[0] - (pole_width // 2), y1
139
+ xp2, yp2 = center[0] - (pole_width // 2), center[1] + flag_size
136
140
 
137
141
  # Draw flag outline using _polygon_outline
138
142
  points = [(x1, y1), (x2, y2), (x3, y3)]
139
143
  layer = Drawable._polygon_outline(layer, points, 1, flag_color, flag_color)
140
-
141
144
  # Draw pole using _line
142
145
  layer = Drawable._line(layer, xp1, yp1, xp2, yp2, pole_color, pole_width)
143
-
144
146
  return layer
145
147
 
146
148
  @staticmethod
147
- def point_inside(x: int, y: int, points) -> bool:
149
+ def point_inside(x: int, y: int, points: list[Tuple[int, int]]) -> bool:
148
150
  """
149
151
  Check if a point (x, y) is inside a polygon defined by a list of points.
150
- Utility to establish the fill point of the geometry.
151
- Parameters:
152
- - x, y: Coordinates of the point to check.
153
- - points: List of (x, y) coordinates defining the polygon.
154
-
155
- Returns:
156
- - True if the point is inside the polygon, False otherwise.
157
152
  """
158
153
  n = len(points)
159
154
  inside = False
160
- xinters = 0
155
+ xinters = 0.0
161
156
  p1x, p1y = points[0]
162
157
  for i in range(1, n + 1):
163
158
  p2x, p2y = points[i % n]
164
159
  if y > min(p1y, p2y):
165
- if y <= max(p1y, p2y):
166
- if x <= max(p1x, p2x):
167
- if p1y != p2y:
168
- xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
169
- if p1x == p2x or x <= xinters:
170
- inside = not inside
160
+ if y <= max(p1y, p2y) and x <= max(p1x, p2x):
161
+ if p1y != p2y:
162
+ xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
163
+ if p1x == p2x or x <= xinters:
164
+ inside = not inside
171
165
  p1x, p1y = p2x, p2y
172
-
173
166
  return inside
174
167
 
175
168
  @staticmethod
@@ -183,47 +176,29 @@ class Drawable:
183
176
  width: int = 3,
184
177
  ) -> NumpyArray:
185
178
  """
186
- Draw a line on a NumPy array (layer) from point A to B.
187
- Parameters:
188
- - layer: NumPy array representing the image.
189
- - x1, y1: Starting coordinates of the line.
190
- - x2, y2: Ending coordinates of the line.
191
- - color: Color of the line (e.g., [R, G, B] or [R, G, B, A] for RGBA).
192
- - width: Width of the line (default is 3).
193
-
194
- Returns:
195
- - Modified layer with the line drawn.
179
+ Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
196
180
  """
197
- # Ensure the coordinates are integers
198
181
  x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
199
-
200
- # Use Bresenham's line algorithm to get the coordinates of the line pixels
201
182
  dx = abs(x2 - x1)
202
183
  dy = abs(y2 - y1)
203
184
  sx = 1 if x1 < x2 else -1
204
185
  sy = 1 if y1 < y2 else -1
205
186
  err = dx - dy
206
-
207
187
  while True:
208
- # Draw a rectangle with the specified width at the current coordinates
188
+ # Draw a rectangle at the current coordinates with the specified width
209
189
  for i in range(-width // 2, (width + 1) // 2):
210
190
  for j in range(-width // 2, (width + 1) // 2):
211
191
  if 0 <= x1 + i < layer.shape[1] and 0 <= y1 + j < layer.shape[0]:
212
192
  layer[y1 + j, x1 + i] = color
213
-
214
193
  if x1 == x2 and y1 == y2:
215
194
  break
216
-
217
195
  e2 = 2 * err
218
-
219
196
  if e2 > -dy:
220
197
  err -= dy
221
198
  x1 += sx
222
-
223
199
  if e2 < dx:
224
200
  err += dx
225
201
  y1 += sy
226
-
227
202
  return layer
228
203
 
229
204
  @staticmethod
@@ -243,23 +218,20 @@ class Drawable:
243
218
  @staticmethod
244
219
  async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
245
220
  """
246
- it joins the coordinates creating a continues line.
247
- the result is our path.
221
+ Join the coordinates creating a continuous line (path).
248
222
  """
249
223
  for coord in coords:
250
- # Use Bresenham's line algorithm to get the coordinates of the line pixels
251
224
  x0, y0 = coord[0]
252
225
  try:
253
226
  x1, y1 = coord[1]
254
227
  except IndexError:
255
- x1 = x0
256
- y1 = y0
228
+ x1, y1 = x0, y0
257
229
  dx = abs(x1 - x0)
258
230
  dy = abs(y1 - y0)
259
231
  sx = 1 if x0 < x1 else -1
260
232
  sy = 1 if y0 < y1 else -1
261
233
  err = dx - dy
262
- line_pixels = []
234
+ line_pixels: list[Tuple[int, int]] = []
263
235
  while True:
264
236
  line_pixels.append((x0, y0))
265
237
  if x0 == x1 and y0 == y1:
@@ -271,14 +243,13 @@ class Drawable:
271
243
  if e2 < dx:
272
244
  err += dx
273
245
  y0 += sy
274
-
275
- # Iterate over the line pixels and draw filled rectangles with the specified width
246
+ # Draw filled rectangles for each pixel in the line
276
247
  for pixel in line_pixels:
277
248
  x, y = pixel
278
249
  for i in range(width):
279
250
  for j in range(width):
280
- if 0 <= x + i < arr.shape[0] and 0 <= y + j < arr.shape[1]:
281
- arr[y + i, x + j] = color
251
+ if 0 <= x + i < arr.shape[1] and 0 <= y + j < arr.shape[0]:
252
+ arr[y + j, x + i] = color
282
253
  return arr
283
254
 
284
255
  @staticmethod
@@ -292,30 +263,47 @@ class Drawable:
292
263
  ) -> NumpyArray:
293
264
  """
294
265
  Draw a filled circle on the image using NumPy.
295
-
296
- Parameters:
297
- - image: NumPy array representing the image.
298
- - center: Center coordinates of the circle (x, y).
299
- - radius: Radius of the circle.
300
- - color: Color of the circle (e.g., [R, G, B] or [R, G, B, A] for RGBA).
301
-
302
- Returns:
303
- - Modified image with the filled circle drawn.
304
266
  """
305
267
  y, x = center
306
268
  rr, cc = np.ogrid[: image.shape[0], : image.shape[1]]
307
269
  circle = (rr - x) ** 2 + (cc - y) ** 2 <= radius**2
308
270
  image[circle] = color
309
271
  if outline_width > 0:
310
- # Create a mask for the outer circle
311
272
  outer_circle = (rr - x) ** 2 + (cc - y) ** 2 <= (
312
273
  radius + outline_width
313
274
  ) ** 2
314
- # Create a mask for the outline by subtracting the inner circle mask from the outer circle mask
315
275
  outline_mask = outer_circle & ~circle
316
- # Fill the outline with the outline color
317
276
  image[outline_mask] = outline_color
277
+ return image
318
278
 
279
+ @staticmethod
280
+ def _filled_circle_optimized(
281
+ image: np.ndarray,
282
+ center: Tuple[int, int],
283
+ radius: int,
284
+ color: Color,
285
+ outline_color: Color = None,
286
+ outline_width: int = 0,
287
+ ) -> np.ndarray:
288
+ """
289
+ Optimized _filled_circle ensuring dtype compatibility with uint8.
290
+ """
291
+ x, y = center
292
+ h, w = image.shape[:2]
293
+ color_np = np.array(color, dtype=image.dtype)
294
+ outline_color_np = (
295
+ np.array(outline_color, dtype=image.dtype)
296
+ if outline_color is not None
297
+ else None
298
+ )
299
+ y_indices, x_indices = np.meshgrid(np.arange(h), np.arange(w), indexing="ij")
300
+ dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
301
+ circle_mask = dist_sq <= radius**2
302
+ image[circle_mask] = color_np
303
+ if outline_width > 0 and outline_color_np is not None:
304
+ outer_mask = dist_sq <= (radius + outline_width) ** 2
305
+ outline_mask = outer_mask & ~circle_mask
306
+ image[outline_mask] = outline_color_np
319
307
  return image
320
308
 
321
309
  @staticmethod
@@ -325,32 +313,26 @@ class Drawable:
325
313
  """
326
314
  Draw an ellipse on the image using NumPy.
327
315
  """
328
- # Create a copy of the image to avoid modifying the original
329
- result_image = image
330
- # Calculate the coordinates of the ellipse's bounding box
331
316
  x, y = center
332
317
  x1, y1 = x - radius, y - radius
333
318
  x2, y2 = x + radius, y + radius
334
- # Draw the filled ellipse
335
- result_image[y1:y2, x1:x2] = color
336
- return result_image
319
+ image[y1:y2, x1:x2] = color
320
+ return image
337
321
 
338
322
  @staticmethod
339
323
  def _polygon_outline(
340
324
  arr: NumpyArray,
341
- points,
325
+ points: list[Tuple[int, int]],
342
326
  width: int,
343
327
  outline_color: Color,
344
328
  fill_color: Color = None,
345
329
  ) -> NumpyArray:
346
330
  """
347
- Draw the outline of a filled polygon on the array using _line.
331
+ Draw the outline of a polygon on the array using _line, and optionally fill it.
348
332
  """
349
- for i, point in enumerate(points):
350
- # Get the current and next points to draw a line between them
333
+ for i, _ in enumerate(points):
351
334
  current_point = points[i]
352
- next_point = points[(i + 1) % len(points)] # Wrap around to the first point
353
- # Use the _line function to draw a line between the current and next points
335
+ next_point = points[(i + 1) % len(points)]
354
336
  arr = Drawable._line(
355
337
  arr,
356
338
  current_point[0],
@@ -360,13 +342,11 @@ class Drawable:
360
342
  outline_color,
361
343
  width,
362
344
  )
363
- # Fill the polygon area with the specified fill color
364
345
  if fill_color is not None:
365
- min_x = min(point[0] for point in points)
366
- max_x = max(point[0] for point in points)
367
- min_y = min(point[1] for point in points)
368
- max_y = max(point[1] for point in points)
369
- # check if we are inside the area and set the color
346
+ min_x = min(p[0] for p in points)
347
+ max_x = max(p[0] for p in points)
348
+ min_y = min(p[1] for p in points)
349
+ max_y = max(p[1] for p in points)
370
350
  for x in range(min_x, max_x + 1):
371
351
  for y in range(min_y, max_y + 1):
372
352
  if Drawable.point_inside(x, y, points):
@@ -378,17 +358,14 @@ class Drawable:
378
358
  """
379
359
  Draw the zones on the input layer.
380
360
  """
381
- dot_radius = 1 # number of pixels the dot should be
382
- dot_spacing = 4 # space between dots.
383
- # Iterate over zones
361
+ dot_radius = 1 # Number of pixels for the dot
362
+ dot_spacing = 4 # Space between dots
384
363
  for zone in coordinates:
385
364
  points = zone["points"]
386
- # determinate the points to cover.
387
365
  min_x = min(points[::2])
388
366
  max_x = max(points[::2])
389
367
  min_y = min(points[1::2])
390
368
  max_y = max(points[1::2])
391
- # Draw ellipses (dots)
392
369
  for y in range(min_y, max_y, dot_spacing):
393
370
  for x in range(min_x, max_x, dot_spacing):
394
371
  for _ in range(dot_radius):
@@ -405,40 +382,29 @@ class Drawable:
405
382
  robot_state: str | None = None,
406
383
  ) -> NumpyArray:
407
384
  """
408
- We Draw the robot with in a smaller array
409
- this helps numpy to work faster and at lower
410
- memory cost.
385
+ Draw the robot on a smaller array to reduce memory cost.
411
386
  """
412
- # Create a 52*52 empty image numpy array of the background
413
387
  top_left_x = x - 26
414
388
  top_left_y = y - 26
415
389
  bottom_right_x = top_left_x + 52
416
390
  bottom_right_y = top_left_y + 52
417
391
  tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
418
- # centre of the above array is used from the rest of the code.
419
- # to draw the robot.
420
392
  tmp_x, tmp_y = 26, 26
421
- # Draw Robot
422
- radius = 25 # Radius of the vacuum constant
423
- r_scaled = radius // 11 # Offset scale for placement of the objects.
424
- r_cover = r_scaled * 12 # Scale factor for cover
425
- lidar_angle = np.deg2rad(
426
- angle + 90
427
- ) # Convert angle to radians and adjust for LIDAR orientation
428
- r_lidar = r_scaled * 3 # Scale factor for the lidar
429
- r_button = r_scaled * 1 # scale factor of the button
430
- # Outline colour from fill colour
393
+ radius = 25
394
+ r_scaled = radius // 11
395
+ r_cover = r_scaled * 12
396
+ lidar_angle = np.deg2rad(angle + 90)
397
+ r_lidar = r_scaled * 3
398
+ r_button = r_scaled * 1
431
399
  if robot_state == "error":
432
400
  outline = Drawable.ERROR_OUTLINE
433
401
  fill = Drawable.ERROR_COLOR
434
402
  else:
435
403
  outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
436
- # Draw the robot outline
437
404
  tmp_layer = Drawable._filled_circle(
438
405
  tmp_layer, (tmp_x, tmp_y), radius, fill, outline, 1
439
406
  )
440
- # Draw bin cover
441
- angle -= 90 # we remove 90 for the cover orientation
407
+ angle -= 90
442
408
  a1 = ((angle + 90) - 80) / 180 * math.pi
443
409
  a2 = ((angle + 90) + 80) / 180 * math.pi
444
410
  x1 = int(tmp_x - r_cover * math.sin(a1))
@@ -446,25 +412,17 @@ class Drawable:
446
412
  x2 = int(tmp_x - r_cover * math.sin(a2))
447
413
  y2 = int(tmp_y + r_cover * math.cos(a2))
448
414
  tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
449
- # Draw Lidar
450
- lidar_x = int(tmp_x + 15 * np.cos(lidar_angle)) # Calculate LIDAR x-coordinate
451
- lidar_y = int(tmp_y + 15 * np.sin(lidar_angle)) # Calculate LIDAR y-coordinate
415
+ lidar_x = int(tmp_x + 15 * np.cos(lidar_angle))
416
+ lidar_y = int(tmp_y + 15 * np.sin(lidar_angle))
452
417
  tmp_layer = Drawable._filled_circle(
453
418
  tmp_layer, (lidar_x, lidar_y), r_lidar, outline
454
419
  )
455
- # Draw Button
456
- butt_x = int(
457
- tmp_x - 20 * np.cos(lidar_angle)
458
- ) # Calculate the button x-coordinate
459
- butt_y = int(
460
- tmp_y - 20 * np.sin(lidar_angle)
461
- ) # Calculate the button y-coordinate
420
+ butt_x = int(tmp_x - 20 * np.cos(lidar_angle))
421
+ butt_y = int(tmp_y - 20 * np.sin(lidar_angle))
462
422
  tmp_layer = Drawable._filled_circle(
463
423
  tmp_layer, (butt_x, butt_y), r_button, outline
464
424
  )
465
- # at last overlay the new robot image to the layer in input.
466
425
  layers = Drawable.overlay_robot(layers, tmp_layer, x, y)
467
- # return the new layer as np array.
468
426
  return layers
469
427
 
470
428
  @staticmethod
@@ -473,50 +431,55 @@ class Drawable:
473
431
  ) -> NumpyArray:
474
432
  """
475
433
  Overlay the robot image on the background image at the specified coordinates.
476
- @param background_image:
477
- @param robot_image:
478
- @param robot x:
479
- @param robot y:
480
- @return: robot image overlaid on the background image.
481
434
  """
482
- # Calculate the dimensions of the robot image
483
435
  robot_height, robot_width, _ = robot_image.shape
484
- # Calculate the center of the robot image (in case const changes)
485
436
  robot_center_x = robot_width // 2
486
437
  robot_center_y = robot_height // 2
487
- # Calculate the position to overlay the robot on the background image
488
438
  top_left_x = x - robot_center_x
489
439
  top_left_y = y - robot_center_y
490
440
  bottom_right_x = top_left_x + robot_width
491
441
  bottom_right_y = top_left_y + robot_height
492
- # Overlay the robot on the background image
493
442
  background_image[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = (
494
443
  robot_image
495
444
  )
496
445
  return background_image
497
446
 
498
447
  @staticmethod
499
- def draw_obstacles(
500
- image: NumpyArray, obstacle_info_list, color: Color
501
- ) -> NumpyArray:
448
+ def draw_filled_circle(
449
+ image: np.ndarray,
450
+ centers: Tuple[int, int],
451
+ radius: int,
452
+ color: Tuple[int, int, int, int],
453
+ ) -> np.ndarray:
502
454
  """
503
- Draw filled circles for obstacles on the image.
504
- Parameters:
505
- - image: NumPy array representing the image.
506
- - obstacle_info_list: List of dictionaries containing obstacle information.
507
- Returns:
508
- - Modified image with filled circles for obstacles.
455
+ Draw multiple filled circles at once using a single NumPy mask.
509
456
  """
510
- for obstacle_info in obstacle_info_list:
511
- enter = obstacle_info.get("points", {})
512
- # label = obstacle_info.get("label", {})
513
- center = (enter["x"], enter["y"])
457
+ h, w = image.shape[:2]
458
+ y_indices, x_indices = np.ogrid[:h, :w] # Precompute coordinate grids
459
+ mask = np.zeros((h, w), dtype=bool)
460
+ for cx, cy in centers:
461
+ mask |= (x_indices - cx) ** 2 + (y_indices - cy) ** 2 <= radius**2
462
+ image[mask] = color
463
+ return image
514
464
 
515
- radius = 6
465
+ @staticmethod
466
+ async def async_draw_obstacles(
467
+ image: np.ndarray, obstacle_info_list, color: Tuple[int, int, int, int]
468
+ ) -> np.ndarray:
469
+ """
470
+ Optimized async version of draw_obstacles using asyncio.gather().
471
+ """
516
472
 
517
- # Draw filled circle
518
- image = Drawable._filled_circle(image, center, radius, color)
473
+ def extract_centers(obs_list) -> np.ndarray:
474
+ return np.array(
475
+ [[obs["points"]["x"], obs["points"]["y"]] for obs in obs_list],
476
+ dtype=np.int32,
477
+ )
519
478
 
479
+ centers = await asyncio.get_running_loop().run_in_executor(
480
+ None, extract_centers, obstacle_info_list
481
+ )
482
+ Drawable.draw_filled_circle(image, centers, 6, color)
520
483
  return image
521
484
 
522
485
  @staticmethod
@@ -528,32 +491,24 @@ class Drawable:
528
491
  path_font: str,
529
492
  position: bool,
530
493
  ) -> None:
531
- """Draw the Status Test on the image."""
532
- # Load a fonts
494
+ """Draw the status text on the image."""
533
495
  path_default_font = (
534
496
  "custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
535
497
  )
536
498
  default_font = ImageFont.truetype(path_default_font, size)
537
499
  user_font = ImageFont.truetype(path_font, size)
538
- # Define the text and position
539
500
  if position:
540
501
  x, y = 10, 10
541
502
  else:
542
503
  x, y = 10, image.height - 20 - size
543
- # Create a drawing object
544
504
  draw = ImageDraw.Draw(image)
545
- # Draw the text
546
505
  for text in status:
547
506
  if "\u2211" in text or "\u03de" in text:
548
507
  font = default_font
549
508
  width = None
550
509
  else:
551
510
  font = user_font
552
- is_variable = path_font.endswith("VT.ttf")
553
- if is_variable:
554
- width = 2
555
- else:
556
- width = None
511
+ width = 2 if path_font.endswith("VT.ttf") else None
557
512
  if width:
558
513
  draw.text((x, y), text, font=font, fill=color, stroke_width=width)
559
514
  else: