valetudo-map-parser 0.1.9b46__py3-none-any.whl → 0.1.9b48__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.
@@ -1,7 +1,7 @@
1
1
  """Valetudo map parser.
2
2
  Version: 0.1.9"""
3
3
 
4
- from .config.colors import ColorsManagment
4
+ from .config.colors import ColorsManagement
5
5
  from .config.drawable import Drawable
6
6
  from .config.drawable_elements import DrawableElement, DrawingConfig
7
7
  from .config.enhanced_drawable import EnhancedDrawable
@@ -25,7 +25,7 @@ __all__ = [
25
25
  "RRMapParser",
26
26
  "CameraShared",
27
27
  "CameraSharedManager",
28
- "ColorsManagment",
28
+ "ColorsManagement",
29
29
  "Drawable",
30
30
  "DrawableElement",
31
31
  "DrawingConfig",
@@ -0,0 +1,51 @@
1
+ """Utility functions for color operations in the map parser."""
2
+
3
+ from typing import Tuple, Optional
4
+
5
+ from SCR.valetudo_map_parser.config.colors import ColorsManagement
6
+ from SCR.valetudo_map_parser.config.types import NumpyArray, Color
7
+
8
+
9
+ def get_blended_color(
10
+ x0: int,
11
+ y0: int,
12
+ x1: int,
13
+ y1: int,
14
+ arr: Optional[NumpyArray],
15
+ color: Color,
16
+ ) -> Color:
17
+ """
18
+ Get a blended color for a pixel based on the current element map and the new element to draw.
19
+
20
+ This function:
21
+ 1. Gets the current element at position (x,y) from the element map
22
+ 2. Gets the color for that element from the colors manager
23
+ 3. Blends the new color with the existing color based on alpha values
24
+
25
+ Returns:
26
+ Blended RGBA color to use for drawing
27
+ """
28
+ # Sample background color at the endpoints and blend with foreground color
29
+ # This is more efficient than sampling at every pixel
30
+ if 0 <= y0 < arr.shape[0] and 0 <= x0 < arr.shape[1]:
31
+ start_blended_color = ColorsManagement.sample_and_blend_color(
32
+ arr, x0, y0, color
33
+ )
34
+ else:
35
+ start_blended_color = color
36
+
37
+ if 0 <= y1 < arr.shape[0] and 0 <= x1 < arr.shape[1]:
38
+ end_blended_color = ColorsManagement.sample_and_blend_color(
39
+ arr, x1, y1, color
40
+ )
41
+ else:
42
+ end_blended_color = color
43
+
44
+ # Use the average of the two blended colors
45
+ blended_color = (
46
+ (start_blended_color[0] + end_blended_color[0]) // 2,
47
+ (start_blended_color[1] + end_blended_color[1]) // 2,
48
+ (start_blended_color[2] + end_blended_color[2]) // 2,
49
+ (start_blended_color[3] + end_blended_color[3]) // 2,
50
+ )
51
+ return blended_color
@@ -456,7 +456,7 @@ class ColorsManagement:
456
456
  out_g = max(0, min(255, out_g))
457
457
  out_b = max(0, min(255, out_b))
458
458
 
459
- return Color(out_r, out_g, out_b, out_a)
459
+ return [out_r, out_g, out_b, out_a]
460
460
 
461
461
  @staticmethod
462
462
  def sample_and_blend_color(array, x: int, y: int, foreground: Color) -> Color:
@@ -3,21 +3,22 @@ Collections of Drawing Utility
3
3
  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
- Version: v2024.12.0
6
+ Version: v0.1.10
7
7
  Refactored for clarity, consistency, and optimized parameter usage.
8
+ Optimized with NumPy and SciPy for better performance.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
11
12
 
12
- import asyncio
13
+
13
14
  import logging
14
15
  import math
15
16
 
16
17
  import numpy as np
17
18
  from PIL import ImageDraw, ImageFont
18
- from scipy import ndimage
19
19
 
20
20
  from .colors import ColorsManagement
21
+ from .color_utils import get_blended_color
21
22
  from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
22
23
 
23
24
 
@@ -62,6 +63,9 @@ class Drawable:
62
63
  # Create the full color with alpha
63
64
  full_color = color if len(color) == 4 else (*color, 255)
64
65
 
66
+ # Check if we need to blend colors (alpha < 255)
67
+ need_blending = alpha < 255
68
+
65
69
  # Loop through pixels to find min and max coordinates
66
70
  for x, y, z in pixels:
67
71
  col = x * pixel_size
@@ -69,13 +73,31 @@ class Drawable:
69
73
  # Draw pixels as blocks
70
74
  for i in range(z):
71
75
  # Get the region to update
72
- region = image_array[
73
- row : row + pixel_size,
74
- col + i * pixel_size : col + (i + 1) * pixel_size,
75
- ]
76
+ region_slice = (
77
+ slice(row, row + pixel_size),
78
+ slice(col + i * pixel_size, col + (i + 1) * pixel_size)
79
+ )
76
80
 
77
- # Simple direct assignment - ignore alpha for now to ensure visibility
78
- region[:] = full_color
81
+ if need_blending:
82
+ # Sample the center of the region for blending
83
+ center_y = row + pixel_size // 2
84
+ center_x = col + i * pixel_size + pixel_size // 2
85
+
86
+ # Only blend if coordinates are valid
87
+ if (0 <= center_y < image_array.shape[0] and
88
+ 0 <= center_x < image_array.shape[1]):
89
+ # Get blended color
90
+ blended_color = ColorsManagement.sample_and_blend_color(
91
+ image_array, center_x, center_y, full_color
92
+ )
93
+ # Apply blended color to the region
94
+ image_array[region_slice] = blended_color
95
+ else:
96
+ # Use original color if out of bounds
97
+ image_array[region_slice] = full_color
98
+ else:
99
+ # No blending needed, use direct assignment
100
+ image_array[region_slice] = full_color
79
101
 
80
102
  return image_array
81
103
 
@@ -83,14 +105,44 @@ class Drawable:
83
105
  async def battery_charger(
84
106
  layers: NumpyArray, x: int, y: int, color: Color
85
107
  ) -> NumpyArray:
86
- """Draw the battery charger on the input layer."""
108
+ """Draw the battery charger on the input layer with color blending."""
109
+ # Check if coordinates are within bounds
110
+ height, width = layers.shape[:2]
111
+ if not (0 <= x < width and 0 <= y < height):
112
+ return layers
113
+
114
+ # Calculate charger dimensions
87
115
  charger_width = 10
88
116
  charger_height = 20
89
- start_row = y - charger_height // 2
90
- end_row = start_row + charger_height
91
- start_col = x - charger_width // 2
92
- end_col = start_col + charger_width
93
- layers[start_row:end_row, start_col:end_col] = color
117
+ start_row = max(0, y - charger_height // 2)
118
+ end_row = min(height, start_row + charger_height)
119
+ start_col = max(0, x - charger_width // 2)
120
+ end_col = min(width, start_col + charger_width)
121
+
122
+ # Skip if charger is completely outside the image
123
+ if start_row >= end_row or start_col >= end_col:
124
+ return layers
125
+
126
+ # Extract alpha from color
127
+ alpha = color[3] if len(color) == 4 else 255
128
+
129
+ # Check if we need to blend colors (alpha < 255)
130
+ if alpha < 255:
131
+ # Sample the center of the charger for blending
132
+ center_y = (start_row + end_row) // 2
133
+ center_x = (start_col + end_col) // 2
134
+
135
+ # Get blended color
136
+ blended_color = ColorsManagement.sample_and_blend_color(
137
+ layers, center_x, center_y, color
138
+ )
139
+
140
+ # Apply blended color
141
+ layers[start_row:end_row, start_col:end_col] = blended_color
142
+ else:
143
+ # No blending needed, use direct assignment
144
+ layers[start_row:end_row, start_col:end_col] = color
145
+
94
146
  return layers
95
147
 
96
148
  @staticmethod
@@ -100,8 +152,34 @@ class Drawable:
100
152
  """
101
153
  Draw a flag centered at specified coordinates on the input layer.
102
154
  It uses the rotation angle of the image to orient the flag.
155
+ Includes color blending for better visual integration.
103
156
  """
104
- pole_color: Color = (0, 0, 255, 255) # Blue for the pole
157
+ # Check if coordinates are within bounds
158
+ height, width = layer.shape[:2]
159
+ x, y = center
160
+ if not (0 <= x < width and 0 <= y < height):
161
+ return layer
162
+
163
+ # Get blended colors for flag and pole
164
+ flag_alpha = flag_color[3] if len(flag_color) == 4 else 255
165
+ pole_color_base = (0, 0, 255) # Blue for the pole
166
+ pole_alpha = 255
167
+
168
+ # Blend flag color if needed
169
+ if flag_alpha < 255:
170
+ flag_color = ColorsManagement.sample_and_blend_color(
171
+ layer, x, y, flag_color
172
+ )
173
+
174
+ # Create pole color with alpha
175
+ pole_color: Color = (*pole_color_base, pole_alpha)
176
+
177
+ # Blend pole color if needed
178
+ if pole_alpha < 255:
179
+ pole_color = ColorsManagement.sample_and_blend_color(
180
+ layer, x, y, pole_color
181
+ )
182
+
105
183
  flag_size = 50
106
184
  pole_width = 6
107
185
  # Adjust flag coordinates based on rotation angle
@@ -174,10 +252,9 @@ class Drawable:
174
252
  y2: int,
175
253
  color: Color,
176
254
  width: int = 3,
177
- blend: bool = True,
178
255
  ) -> NumpyArray:
179
256
  """
180
- Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
257
+ Draw a line on a NumPy array (layer) from point A to B using vectorized operations.
181
258
 
182
259
  Args:
183
260
  layer: The numpy array to draw on
@@ -185,59 +262,45 @@ class Drawable:
185
262
  x2, y2: End point coordinates
186
263
  color: Color to draw with
187
264
  width: Width of the line
188
- blend: Whether to blend the color with the background
189
265
  """
190
-
266
+ # Ensure coordinates are integers
191
267
  x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
192
268
 
193
- # Sample background color at the endpoints and blend with foreground color if requested
194
- if blend:
195
- # Sample at start point
196
- if 0 <= y1 < layer.shape[0] and 0 <= x1 < layer.shape[1]:
197
- start_blended_color = ColorsManagement.sample_and_blend_color(
198
- layer, x1, y1, color
199
- )
200
- else:
201
- start_blended_color = color
269
+ # Get blended color for the line
270
+ blended_color = get_blended_color(x1, y1, x2, y2, layer, color)
202
271
 
203
- # Sample at end point
204
- if 0 <= y2 < layer.shape[0] and 0 <= x2 < layer.shape[1]:
205
- end_blended_color = ColorsManagement.sample_and_blend_color(
206
- layer, x2, y2, color
207
- )
208
- else:
209
- end_blended_color = color
210
-
211
- # Use the average of the two blended colors
212
- blended_color = (
213
- (start_blended_color[0] + end_blended_color[0]) // 2,
214
- (start_blended_color[1] + end_blended_color[1]) // 2,
215
- (start_blended_color[2] + end_blended_color[2]) // 2,
216
- (start_blended_color[3] + end_blended_color[3]) // 2,
217
- )
218
- else:
219
- blended_color = color
220
-
221
- dx = abs(x2 - x1)
222
- dy = abs(y2 - y1)
223
- sx = 1 if x1 < x2 else -1
224
- sy = 1 if y1 < y2 else -1
225
- err = dx - dy
226
- while True:
227
- # Draw a rectangle at the current coordinates with the specified width
272
+ # Calculate line length
273
+ length = max(abs(x2 - x1), abs(y2 - y1))
274
+ if length == 0: # Handle case of a single point
275
+ # Draw a dot with the specified width
228
276
  for i in range(-width // 2, (width + 1) // 2):
229
277
  for j in range(-width // 2, (width + 1) // 2):
230
278
  if 0 <= x1 + i < layer.shape[1] and 0 <= y1 + j < layer.shape[0]:
231
279
  layer[y1 + j, x1 + i] = blended_color
232
- if x1 == x2 and y1 == y2:
233
- break
234
- e2 = 2 * err
235
- if e2 > -dy:
236
- err -= dy
237
- x1 += sx
238
- if e2 < dx:
239
- err += dx
240
- y1 += sy
280
+ return layer
281
+
282
+ # Create parametric points along the line
283
+ t = np.linspace(0, 1, length * 2) # Double the points for smoother lines
284
+ x_coords = np.round(x1 * (1 - t) + x2 * t).astype(int)
285
+ y_coords = np.round(y1 * (1 - t) + y2 * t).astype(int)
286
+
287
+ # Draw the line with the specified width
288
+ if width == 1:
289
+ # Fast path for width=1
290
+ for x, y in zip(x_coords, y_coords):
291
+ if 0 <= x < layer.shape[1] and 0 <= y < layer.shape[0]:
292
+ layer[y, x] = blended_color
293
+ else:
294
+ # For thicker lines, draw a rectangle at each point
295
+ half_width = width // 2
296
+ for x, y in zip(x_coords, y_coords):
297
+ for i in range(-half_width, half_width + 1):
298
+ for j in range(-half_width, half_width + 1):
299
+ if (i*i + j*j <= half_width*half_width and # Make it round
300
+ 0 <= x + i < layer.shape[1] and
301
+ 0 <= y + j < layer.shape[0]):
302
+ layer[y + j, x + i] = blended_color
303
+
241
304
  return layer
242
305
 
243
306
  @staticmethod
@@ -258,8 +321,8 @@ class Drawable:
258
321
  async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
259
322
  """
260
323
  Join the coordinates creating a continuous line (path).
324
+ Optimized with vectorized operations for better performance.
261
325
  """
262
-
263
326
  for coord in coords:
264
327
  x0, y0 = coord[0]
265
328
  try:
@@ -267,55 +330,16 @@ class Drawable:
267
330
  except IndexError:
268
331
  x1, y1 = x0, y0
269
332
 
270
- # Sample background color at the endpoints and blend with foreground color
271
- # This is more efficient than sampling at every pixel
272
- if 0 <= y0 < arr.shape[0] and 0 <= x0 < arr.shape[1]:
273
- start_blended_color = ColorsManagement.sample_and_blend_color(
274
- arr, x0, y0, color
275
- )
276
- else:
277
- start_blended_color = color
333
+ # Skip if coordinates are the same
334
+ if x0 == x1 and y0 == y1:
335
+ continue
278
336
 
279
- if 0 <= y1 < arr.shape[0] and 0 <= x1 < arr.shape[1]:
280
- end_blended_color = ColorsManagement.sample_and_blend_color(
281
- arr, x1, y1, color
282
- )
283
- else:
284
- end_blended_color = color
285
-
286
- # Use the average of the two blended colors
287
- blended_color = (
288
- (start_blended_color[0] + end_blended_color[0]) // 2,
289
- (start_blended_color[1] + end_blended_color[1]) // 2,
290
- (start_blended_color[2] + end_blended_color[2]) // 2,
291
- (start_blended_color[3] + end_blended_color[3]) // 2,
292
- )
337
+ # Get blended color for this line segment
338
+ blended_color = get_blended_color(x0, y0, x1, y1, arr, color)
339
+
340
+ # Use the optimized line drawing method
341
+ arr = Drawable._line(arr, x0, y0, x1, y1, blended_color, width)
293
342
 
294
- dx = abs(x1 - x0)
295
- dy = abs(y1 - y0)
296
- sx = 1 if x0 < x1 else -1
297
- sy = 1 if y0 < y1 else -1
298
- err = dx - dy
299
- line_pixels: list[Tuple[int, int]] = []
300
- while True:
301
- line_pixels.append((x0, y0))
302
- if x0 == x1 and y0 == y1:
303
- break
304
- e2 = 2 * err
305
- if e2 > -dy:
306
- err -= dy
307
- x0 += sx
308
- if e2 < dx:
309
- err += dx
310
- y0 += sy
311
- # Draw filled rectangles for each pixel in the line
312
- for pixel in line_pixels:
313
- x, y = pixel
314
- for i in range(width):
315
- for j in range(width):
316
- px, py = x + i, y + j
317
- if 0 <= px < arr.shape[1] and 0 <= py < arr.shape[0]:
318
- arr[py, px] = blended_color
319
343
  return arr
320
344
 
321
345
  @staticmethod
@@ -329,17 +353,35 @@ class Drawable:
329
353
  ) -> NumpyArray:
330
354
  """
331
355
  Draw a filled circle on the image using NumPy.
356
+ Optimized to only process the bounding box of the circle.
332
357
  """
333
358
  y, x = center
334
- rr, cc = np.ogrid[: image.shape[0], : image.shape[1]]
335
- circle = (rr - x) ** 2 + (cc - y) ** 2 <= radius**2
336
- image[circle] = color
337
- if outline_width > 0:
338
- outer_circle = (rr - x) ** 2 + (cc - y) ** 2 <= (
339
- radius + outline_width
340
- ) ** 2
341
- outline_mask = outer_circle & ~circle
342
- image[outline_mask] = outline_color
359
+ height, width = image.shape[:2]
360
+
361
+ # Calculate the bounding box of the circle
362
+ min_y = max(0, y - radius - outline_width)
363
+ max_y = min(height, y + radius + outline_width + 1)
364
+ min_x = max(0, x - radius - outline_width)
365
+ max_x = min(width, x + radius + outline_width + 1)
366
+
367
+ # Create coordinate arrays for the bounding box
368
+ y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
369
+
370
+ # Calculate distances from center
371
+ dist_sq = (y_indices - y) ** 2 + (x_indices - x) ** 2
372
+
373
+ # Create masks for the circle and outline
374
+ circle_mask = dist_sq <= radius ** 2
375
+
376
+ # Apply the fill color
377
+ image[min_y:max_y, min_x:max_x][circle_mask] = color
378
+
379
+ # Draw the outline if needed
380
+ if outline_width > 0 and outline_color is not None:
381
+ outer_mask = dist_sq <= (radius + outline_width) ** 2
382
+ outline_mask = outer_mask & ~circle_mask
383
+ image[min_y:max_y, min_x:max_x][outline_mask] = outline_color
384
+
343
385
  return image
344
386
 
345
387
  @staticmethod
@@ -395,7 +437,9 @@ class Drawable:
395
437
  ) -> NumpyArray:
396
438
  """
397
439
  Draw the outline of a polygon on the array using _line, and optionally fill it.
440
+ Uses NumPy vectorized operations for improved performance.
398
441
  """
442
+ # Draw the outline
399
443
  for i, _ in enumerate(points):
400
444
  current_point = points[i]
401
445
  next_point = points[(i + 1) % len(points)]
@@ -408,31 +452,53 @@ class Drawable:
408
452
  outline_color,
409
453
  width,
410
454
  )
411
- if fill_color is not None:
412
- min_x = min(p[0] for p in points)
413
- max_x = max(p[0] for p in points)
414
- min_y = min(p[1] for p in points)
415
- max_y = max(p[1] for p in points)
416
- for x in range(min_x, max_x + 1):
417
- for y in range(min_y, max_y + 1):
418
- if Drawable.point_inside(x, y, points):
419
- arr[y, x] = fill_color
455
+
456
+ # Fill the polygon if a fill color is provided
457
+ if fill_color is not None:
458
+ # Get the bounding box of the polygon
459
+ min_x = max(0, min(p[0] for p in points))
460
+ max_x = min(arr.shape[1] - 1, max(p[0] for p in points))
461
+ min_y = max(0, min(p[1] for p in points))
462
+ max_y = min(arr.shape[0] - 1, max(p[1] for p in points))
463
+
464
+ # Create a mask for the polygon region
465
+ mask = np.zeros((max_y - min_y + 1, max_x - min_x + 1), dtype=bool)
466
+
467
+ # Adjust points to the mask's coordinate system
468
+ adjusted_points = [(p[0] - min_x, p[1] - min_y) for p in points]
469
+
470
+ # Create a grid of coordinates and use it to test all points at once
471
+ y_indices, x_indices = np.mgrid[0:mask.shape[0], 0:mask.shape[1]]
472
+
473
+ # Test each point in the grid
474
+ for i in range(mask.shape[0]):
475
+ for j in range(mask.shape[1]):
476
+ mask[i, j] = Drawable.point_inside(j, i, adjusted_points)
477
+
478
+ # Apply the fill color to the masked region
479
+ arr[min_y:max_y+1, min_x:max_x+1][mask] = fill_color
480
+
420
481
  return arr
421
482
 
422
483
  @staticmethod
423
484
  async def zones(layers: NumpyArray, coordinates, color: Color) -> NumpyArray:
424
485
  """
425
486
  Draw the zones on the input layer with color blending.
487
+ Optimized with NumPy vectorized operations for better performance.
426
488
  """
427
-
428
489
  dot_radius = 1 # Number of pixels for the dot
429
490
  dot_spacing = 4 # Space between dots
491
+
430
492
  for zone in coordinates:
431
493
  points = zone["points"]
432
- min_x = min(points[::2])
433
- max_x = max(points[::2])
434
- min_y = min(points[1::2])
435
- max_y = max(points[1::2])
494
+ min_x = max(0, min(points[::2]))
495
+ max_x = min(layers.shape[1] - 1, max(points[::2]))
496
+ min_y = max(0, min(points[1::2]))
497
+ max_y = min(layers.shape[0] - 1, max(points[1::2]))
498
+
499
+ # Skip if zone is outside the image
500
+ if min_x >= max_x or min_y >= max_y:
501
+ continue
436
502
 
437
503
  # Sample a point from the zone to get the background color
438
504
  # Use the center of the zone for sampling
@@ -447,12 +513,28 @@ class Drawable:
447
513
  else:
448
514
  blended_color = color
449
515
 
450
- for y in range(min_y, max_y, dot_spacing):
451
- for x in range(min_x, max_x, dot_spacing):
452
- for _ in range(dot_radius):
453
- layers = Drawable._ellipse(
454
- layers, (x, y), dot_radius, blended_color
455
- )
516
+ # Create a grid of dot centers
517
+ x_centers = np.arange(min_x, max_x, dot_spacing)
518
+ y_centers = np.arange(min_y, max_y, dot_spacing)
519
+
520
+ # Draw dots at each grid point
521
+ for y in y_centers:
522
+ for x in x_centers:
523
+ # Create a small mask for the dot
524
+ y_min = max(0, y - dot_radius)
525
+ y_max = min(layers.shape[0], y + dot_radius + 1)
526
+ x_min = max(0, x - dot_radius)
527
+ x_max = min(layers.shape[1], x + dot_radius + 1)
528
+
529
+ # Create coordinate arrays for the dot
530
+ y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
531
+
532
+ # Create a circular mask
533
+ mask = (y_indices - y)**2 + (x_indices - x)**2 <= dot_radius**2
534
+
535
+ # Apply the color to the masked region
536
+ layers[y_min:y_max, x_min:x_max][mask] = blended_color
537
+
456
538
  return layers
457
539
 
458
540
  @staticmethod
@@ -466,27 +548,56 @@ class Drawable:
466
548
  ) -> NumpyArray:
467
549
  """
468
550
  Draw the robot on a smaller array to reduce memory cost.
551
+ Optimized with NumPy vectorized operations for better performance.
469
552
  """
470
- top_left_x = x - 26
471
- top_left_y = y - 26
472
- bottom_right_x = top_left_x + 52
473
- bottom_right_y = top_left_y + 52
474
- tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
475
- tmp_x, tmp_y = 26, 26
553
+ # Ensure coordinates are within bounds
554
+ height, width = layers.shape[:2]
555
+ if not (0 <= x < width and 0 <= y < height):
556
+ return layers
557
+
558
+ # Calculate the bounding box for the robot
476
559
  radius = 25
560
+ box_size = radius * 2 + 2 # Add a small margin
561
+
562
+ # Calculate the region to draw on
563
+ top_left_x = max(0, x - radius - 1)
564
+ top_left_y = max(0, y - radius - 1)
565
+ bottom_right_x = min(width, x + radius + 1)
566
+ bottom_right_y = min(height, y + radius + 1)
567
+
568
+ # Skip if the robot is completely outside the image
569
+ if top_left_x >= bottom_right_x or top_left_y >= bottom_right_y:
570
+ return layers
571
+
572
+ # Create a temporary layer for the robot
573
+ tmp_width = bottom_right_x - top_left_x
574
+ tmp_height = bottom_right_y - top_left_y
575
+ tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
576
+
577
+ # Calculate the robot center in the temporary layer
578
+ tmp_x = x - top_left_x
579
+ tmp_y = y - top_left_y
580
+
581
+ # Calculate robot parameters
477
582
  r_scaled = radius // 11
478
583
  r_cover = r_scaled * 12
479
584
  lidar_angle = np.deg2rad(angle + 90)
480
585
  r_lidar = r_scaled * 3
481
586
  r_button = r_scaled * 1
587
+
588
+ # Set colors based on robot state
482
589
  if robot_state == "error":
483
590
  outline = Drawable.ERROR_OUTLINE
484
591
  fill = Drawable.ERROR_COLOR
485
592
  else:
486
593
  outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
594
+
595
+ # Draw the main robot body
487
596
  tmp_layer = Drawable._filled_circle(
488
- tmp_layer, (tmp_x, tmp_y), radius, fill, outline, 1
597
+ tmp_layer, (tmp_y, tmp_x), radius, fill, outline, 1
489
598
  )
599
+
600
+ # Draw the robot direction indicator
490
601
  angle -= 90
491
602
  a1 = ((angle + 90) - 80) / 180 * math.pi
492
603
  a2 = ((angle + 90) + 80) / 180 * math.pi
@@ -494,18 +605,31 @@ class Drawable:
494
605
  y1 = int(tmp_y + r_cover * math.cos(a1))
495
606
  x2 = int(tmp_x - r_cover * math.sin(a2))
496
607
  y2 = int(tmp_y + r_cover * math.cos(a2))
497
- tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
608
+
609
+ # Draw the direction line
610
+ if (0 <= x1 < tmp_width and 0 <= y1 < tmp_height and
611
+ 0 <= x2 < tmp_width and 0 <= y2 < tmp_height):
612
+ tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
613
+
614
+ # Draw the lidar indicator
498
615
  lidar_x = int(tmp_x + 15 * np.cos(lidar_angle))
499
616
  lidar_y = int(tmp_y + 15 * np.sin(lidar_angle))
500
- tmp_layer = Drawable._filled_circle(
501
- tmp_layer, (lidar_x, lidar_y), r_lidar, outline
502
- )
617
+ if 0 <= lidar_x < tmp_width and 0 <= lidar_y < tmp_height:
618
+ tmp_layer = Drawable._filled_circle(
619
+ tmp_layer, (lidar_y, lidar_x), r_lidar, outline
620
+ )
621
+
622
+ # Draw the button indicator
503
623
  butt_x = int(tmp_x - 20 * np.cos(lidar_angle))
504
624
  butt_y = int(tmp_y - 20 * np.sin(lidar_angle))
505
- tmp_layer = Drawable._filled_circle(
506
- tmp_layer, (butt_x, butt_y), r_button, outline
507
- )
508
- layers = Drawable.overlay_robot(layers, tmp_layer, x, y)
625
+ if 0 <= butt_x < tmp_width and 0 <= butt_y < tmp_height:
626
+ tmp_layer = Drawable._filled_circle(
627
+ tmp_layer, (butt_y, butt_x), r_button, outline
628
+ )
629
+
630
+ # Copy the robot layer back to the main layer
631
+ layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = tmp_layer
632
+
509
633
  return layers
510
634
 
511
635
  @staticmethod
@@ -545,24 +669,193 @@ class Drawable:
545
669
  image[mask] = color
546
670
  return image
547
671
 
672
+ @staticmethod
673
+ def batch_draw_elements(
674
+ image: np.ndarray,
675
+ elements: list,
676
+ element_type: str,
677
+ color: Color,
678
+ ) -> np.ndarray:
679
+ """
680
+ Efficiently draw multiple elements of the same type at once.
681
+
682
+ Args:
683
+ image: The image array to draw on
684
+ elements: List of element data (coordinates, etc.)
685
+ element_type: Type of element to draw ('circle', 'line', etc.)
686
+ color: Color to use for drawing
687
+
688
+ Returns:
689
+ Modified image array
690
+ """
691
+ if not elements or len(elements) == 0:
692
+ return image
693
+
694
+ # Get image dimensions
695
+ height, width = image.shape[:2]
696
+
697
+ if element_type == 'circle':
698
+ # Extract circle centers and radii
699
+ centers = []
700
+ radii = []
701
+ for elem in elements:
702
+ if isinstance(elem, dict) and 'center' in elem and 'radius' in elem:
703
+ centers.append(elem['center'])
704
+ radii.append(elem['radius'])
705
+ elif isinstance(elem, (list, tuple)) and len(elem) >= 3:
706
+ # Format: (x, y, radius)
707
+ centers.append((elem[0], elem[1]))
708
+ radii.append(elem[2])
709
+
710
+ # Process circles with the same radius together
711
+ for radius in set(radii):
712
+ same_radius_centers = [centers[i] for i in range(len(centers)) if radii[i] == radius]
713
+ if same_radius_centers:
714
+ # Create a combined mask for all circles with this radius
715
+ mask = np.zeros((height, width), dtype=bool)
716
+ for cx, cy in same_radius_centers:
717
+ if 0 <= cx < width and 0 <= cy < height:
718
+ # Calculate circle bounds
719
+ min_y = max(0, cy - radius)
720
+ max_y = min(height, cy + radius + 1)
721
+ min_x = max(0, cx - radius)
722
+ max_x = min(width, cx + radius + 1)
723
+
724
+ # Create coordinate arrays for the circle
725
+ y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
726
+
727
+ # Add this circle to the mask
728
+ circle_mask = (y_indices - cy)**2 + (x_indices - cx)**2 <= radius**2
729
+ mask[min_y:max_y, min_x:max_x] |= circle_mask
730
+
731
+ # Apply color to all circles at once
732
+ image[mask] = color
733
+
734
+ elif element_type == 'line':
735
+ # Extract line endpoints
736
+ lines = []
737
+ widths = []
738
+ for elem in elements:
739
+ if isinstance(elem, dict) and 'start' in elem and 'end' in elem:
740
+ lines.append((elem['start'], elem['end']))
741
+ widths.append(elem.get('width', 1))
742
+ elif isinstance(elem, (list, tuple)) and len(elem) >= 4:
743
+ # Format: (x1, y1, x2, y2, [width])
744
+ lines.append(((elem[0], elem[1]), (elem[2], elem[3])))
745
+ widths.append(elem[4] if len(elem) > 4 else 1)
746
+
747
+ # Process lines with the same width together
748
+ for width in set(widths):
749
+ same_width_lines = [lines[i] for i in range(len(lines)) if widths[i] == width]
750
+ if same_width_lines:
751
+ # Create a combined mask for all lines with this width
752
+ mask = np.zeros((height, width), dtype=bool)
753
+
754
+ # Draw all lines into the mask
755
+ for (start, end) in same_width_lines:
756
+ x1, y1 = start
757
+ x2, y2 = end
758
+
759
+ # Skip invalid lines
760
+ if not (0 <= x1 < width and 0 <= y1 < height and
761
+ 0 <= x2 < width and 0 <= y2 < height):
762
+ continue
763
+
764
+ # Use Bresenham's algorithm to get line points
765
+ length = max(abs(x2 - x1), abs(y2 - y1))
766
+ if length == 0:
767
+ continue
768
+
769
+ t = np.linspace(0, 1, length * 2)
770
+ x_coords = np.round(x1 * (1 - t) + x2 * t).astype(int)
771
+ y_coords = np.round(y1 * (1 - t) + y2 * t).astype(int)
772
+
773
+ # Add line points to mask
774
+ for x, y in zip(x_coords, y_coords):
775
+ if width == 1:
776
+ mask[y, x] = True
777
+ else:
778
+ # For thicker lines
779
+ half_width = width // 2
780
+ min_y = max(0, y - half_width)
781
+ max_y = min(height, y + half_width + 1)
782
+ min_x = max(0, x - half_width)
783
+ max_x = min(width, x + half_width + 1)
784
+
785
+ # Create a circular brush
786
+ y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
787
+ brush = (y_indices - y)**2 + (x_indices - x)**2 <= half_width**2
788
+ mask[min_y:max_y, min_x:max_x] |= brush
789
+
790
+ # Apply color to all lines at once
791
+ image[mask] = color
792
+
793
+ return image
794
+
548
795
  @staticmethod
549
796
  async def async_draw_obstacles(
550
797
  image: np.ndarray, obstacle_info_list, color: Color
551
798
  ) -> np.ndarray:
552
799
  """
553
- Optimized async version of draw_obstacles using asyncio.gather().
800
+ Optimized async version of draw_obstacles using batch processing.
801
+ Includes color blending for better visual integration.
554
802
  """
803
+ if not obstacle_info_list:
804
+ return image
555
805
 
556
- def extract_centers(obs_list) -> np.ndarray:
557
- return np.array(
558
- [[obs["points"]["x"], obs["points"]["y"]] for obs in obs_list],
559
- dtype=np.int32,
560
- )
806
+ # Extract alpha from color
807
+ alpha = color[3] if len(color) == 4 else 255
808
+ need_blending = alpha < 255
809
+
810
+ # Extract obstacle centers and prepare for batch processing
811
+ centers = []
812
+ for obs in obstacle_info_list:
813
+ try:
814
+ x = obs["points"]["x"]
815
+ y = obs["points"]["y"]
816
+
817
+ # Skip if coordinates are out of bounds
818
+ if not (0 <= x < image.shape[1] and 0 <= y < image.shape[0]):
819
+ continue
820
+
821
+ # Apply color blending if needed
822
+ obstacle_color = color
823
+ if need_blending:
824
+ obstacle_color = ColorsManagement.sample_and_blend_color(
825
+ image, x, y, color
826
+ )
827
+
828
+ # Add to centers list with radius
829
+ centers.append({
830
+ 'center': (x, y),
831
+ 'radius': 6,
832
+ 'color': obstacle_color
833
+ })
834
+ except (KeyError, TypeError):
835
+ continue
836
+
837
+ # Draw each obstacle with its blended color
838
+ if centers:
839
+ for obstacle in centers:
840
+ cx, cy = obstacle['center']
841
+ radius = obstacle['radius']
842
+ obs_color = obstacle['color']
843
+
844
+ # Create a small mask for the obstacle
845
+ min_y = max(0, cy - radius)
846
+ max_y = min(image.shape[0], cy + radius + 1)
847
+ min_x = max(0, cx - radius)
848
+ max_x = min(image.shape[1], cx + radius + 1)
849
+
850
+ # Create coordinate arrays for the circle
851
+ y_indices, x_indices = np.ogrid[min_y:max_y, min_x:max_x]
852
+
853
+ # Create a circular mask
854
+ mask = (y_indices - cy)**2 + (x_indices - cx)**2 <= radius**2
855
+
856
+ # Apply the color to the masked region
857
+ image[min_y:max_y, min_x:max_x][mask] = obs_color
561
858
 
562
- centers = await asyncio.get_running_loop().run_in_executor(
563
- None, extract_centers, obstacle_info_list
564
- )
565
- Drawable.draw_filled_circle(image, centers, 6, color)
566
859
  return image
567
860
 
568
861
  @staticmethod
@@ -597,60 +890,3 @@ class Drawable:
597
890
  else:
598
891
  draw.text((x, y), text, font=font, fill=color)
599
892
  x += draw.textlength(text, font=default_font)
600
-
601
- @staticmethod
602
- def draw_filled_polygon(image_array, vertices, color):
603
- """
604
- Draw a filled polygon on the image array using scipy.ndimage.
605
-
606
- Args:
607
- image_array: NumPy array of shape (height, width, 4) containing RGBA image data
608
- vertices: List of (x,y) tuples defining the polygon vertices
609
- color: RGBA color tuple to fill the polygon with
610
-
611
- Returns:
612
- Modified image array with the filled polygon
613
- """
614
- if len(vertices) < 3:
615
- return image_array # Need at least 3 points for a polygon
616
-
617
- height, width = image_array.shape[:2]
618
-
619
- # Create a mask for the polygon
620
- polygon_mask = np.zeros((height, width), dtype=bool)
621
-
622
- # Convert vertices to numpy arrays for processing
623
- vertices_array = np.array(vertices)
624
- x_coords = vertices_array[:, 0]
625
- y_coords = vertices_array[:, 1]
626
-
627
- # Clip coordinates to image boundaries
628
- x_coords = np.clip(x_coords, 0, width - 1)
629
- y_coords = np.clip(y_coords, 0, height - 1)
630
-
631
- # Create a polygon using scipy.ndimage
632
- # First create the boundary
633
- for i in range(len(vertices)):
634
- x1, y1 = int(x_coords[i]), int(y_coords[i])
635
- x2, y2 = (
636
- int(x_coords[(i + 1) % len(vertices)]),
637
- int(y_coords[(i + 1) % len(vertices)]),
638
- )
639
-
640
- # Draw line between consecutive vertices
641
- length = max(abs(x2 - x1), abs(y2 - y1)) * 2
642
- if length == 0:
643
- continue
644
-
645
- t = np.linspace(0, 1, length)
646
- x = np.round(x1 * (1 - t) + x2 * t).astype(int)
647
- y = np.round(y1 * (1 - t) + y2 * t).astype(int)
648
-
649
- # Add boundary points to mask
650
- polygon_mask[y, x] = True
651
-
652
- # Fill the polygon using scipy.ndimage.binary_fill_holes
653
- filled_polygon = ndimage.binary_fill_holes(polygon_mask)
654
-
655
- # Apply color to the filled polygon
656
- return ColorsManagement.batch_blend_colors(image_array, filled_polygon, color)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b46
3
+ Version: 0.1.9b48
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -1,8 +1,9 @@
1
- valetudo_map_parser/__init__.py,sha256=INujZn4exaXUjqMN8nZkGJIziDWlW59t65fJ34HJX44,956
1
+ valetudo_map_parser/__init__.py,sha256=SOxmq7LkS7eMa2N4atW7ZBbqhGEL7fpj6MsyXZpCMsk,958
2
2
  valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
3
3
  valetudo_map_parser/config/auto_crop.py,sha256=6OvRsWzXMXBaSEvgwpaaisNdozDKiDyTmPjknFxoUMc,12624
4
- valetudo_map_parser/config/colors.py,sha256=90NkvzX7b2ckEWvTQSX6mvUDvS0IuiywMPX6__74iL0,30283
5
- valetudo_map_parser/config/drawable.py,sha256=dZ6FZVch9GVIpT4k52I5iXWtSH9oD9KJArcK48p-OaA,23667
4
+ valetudo_map_parser/config/color_utils.py,sha256=pJ9Frx5aZ1W2XPJxNToBLIwSYNTIm4Tw_NMMfFrZSaM,1691
5
+ valetudo_map_parser/config/colors.py,sha256=zF-6CGe6XA9wtnrWY8hBmqsMNOcL1x5tZY1G6evniQ0,30278
6
+ valetudo_map_parser/config/drawable.py,sha256=9-7FBlzWDfr7d_yIeRWXlAxlRxRfKflxi4fTnC3hqjk,33785
6
7
  valetudo_map_parser/config/drawable_elements.py,sha256=Ulfgf8B4LuLCfx-FfmC7LrP8o9ll_Sncg9mR774_3KE,43140
7
8
  valetudo_map_parser/config/enhanced_drawable.py,sha256=xNgFUNccstP245VgLFEA9gjB3-VvlSAJSjRgSZ3YFL0,16641
8
9
  valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
@@ -18,9 +19,8 @@ valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
18
19
  valetudo_map_parser/rand25_handler.py,sha256=F9o1J6JZRV3CZTS4CG3AHNHZweKRM0nzd4HLmdPZe4w,23617
19
20
  valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
20
21
  valetudo_map_parser/utils/__init__.py,sha256=r-GKKSPqBkMDd2K-vWe7kAix8OBrGN5HXC1RS2tbDwo,130
21
- valetudo_map_parser/utils/color_utils.py,sha256=VogvNcITQHvtMUrVkzKcimvKtwSw0fjH72_RWtNoULA,2394
22
- valetudo_map_parser-0.1.9b46.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
23
- valetudo_map_parser-0.1.9b46.dist-info/METADATA,sha256=7UIQEEVpOu_HR4FEIXeoVHSniZYc3FaNPtfZEJZTvMQ,3321
24
- valetudo_map_parser-0.1.9b46.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
25
- valetudo_map_parser-0.1.9b46.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
26
- valetudo_map_parser-0.1.9b46.dist-info/RECORD,,
22
+ valetudo_map_parser-0.1.9b48.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
23
+ valetudo_map_parser-0.1.9b48.dist-info/METADATA,sha256=5lDbj_xHkH3jIyKf1GKrXLLMVUXLRLuOavw04HkqvMs,3321
24
+ valetudo_map_parser-0.1.9b48.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
25
+ valetudo_map_parser-0.1.9b48.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
26
+ valetudo_map_parser-0.1.9b48.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,62 +0,0 @@
1
- """Utility functions for color operations in the map parser."""
2
-
3
- from typing import Tuple, Optional
4
-
5
- from ..config.colors import ColorsManagment
6
- from ..config.drawable_elements import ElementMapGenerator, DrawableElement
7
-
8
-
9
- def get_blended_color(
10
- element_map_generator: ElementMapGenerator,
11
- colors_manager: ColorsManagment,
12
- x: int,
13
- y: int,
14
- new_element: DrawableElement,
15
- new_color: Tuple[int, int, int, int],
16
- ) -> Tuple[int, int, int, int]:
17
- """
18
- Get a blended color for a pixel based on the current element map and the new element to draw.
19
-
20
- This function:
21
- 1. Gets the current element at position (x,y) from the element map
22
- 2. Gets the color for that element from the colors manager
23
- 3. Blends the new color with the existing color based on alpha values
24
-
25
- Args:
26
- element_map_generator: The element map generator containing the current element map
27
- colors_manager: The colors manager to get colors for elements
28
- x: X coordinate in the element map
29
- y: Y coordinate in the element map
30
- new_element: The new element to draw at this position
31
- new_color: The RGBA color of the new element
32
-
33
- Returns:
34
- Blended RGBA color to use for drawing
35
- """
36
- # Get current element at this position
37
- current_element = element_map_generator.get_element_at_position(x, y)
38
-
39
- # If no current element or it's the same as the new element, just return the new color
40
- if current_element is None or current_element == new_element:
41
- return new_color
42
-
43
- # Get color for the current element
44
- current_color = None
45
-
46
- # Handle different element types
47
- if current_element == DrawableElement.FLOOR:
48
- # Floor is the background color
49
- current_color = colors_manager.get_colour("color_background")
50
- elif current_element == DrawableElement.WALL:
51
- # Wall color
52
- current_color = colors_manager.get_colour("color_wall")
53
- elif DrawableElement.ROOM_1 <= current_element <= DrawableElement.ROOM_15:
54
- # Room colors (ROOM_1 = 16, ROOM_2 = 17, etc.)
55
- room_index = current_element - DrawableElement.ROOM_1
56
- current_color = colors_manager.get_colour(f"color_room_{room_index}")
57
- else:
58
- # Default for unknown elements
59
- current_color = (100, 100, 100, 255)
60
-
61
- # Blend the colors
62
- return colors_manager.blend_colors(current_color, new_color)