valetudo-map-parser 0.1.8__py3-none-any.whl → 0.1.9a2__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.
Files changed (28) hide show
  1. valetudo_map_parser/__init__.py +19 -12
  2. valetudo_map_parser/config/auto_crop.py +174 -116
  3. valetudo_map_parser/config/color_utils.py +105 -0
  4. valetudo_map_parser/config/colors.py +662 -13
  5. valetudo_map_parser/config/drawable.py +624 -279
  6. valetudo_map_parser/config/drawable_elements.py +292 -0
  7. valetudo_map_parser/config/enhanced_drawable.py +324 -0
  8. valetudo_map_parser/config/optimized_element_map.py +406 -0
  9. valetudo_map_parser/config/rand25_parser.py +42 -28
  10. valetudo_map_parser/config/room_outline.py +148 -0
  11. valetudo_map_parser/config/shared.py +73 -6
  12. valetudo_map_parser/config/types.py +102 -51
  13. valetudo_map_parser/config/utils.py +841 -0
  14. valetudo_map_parser/hypfer_draw.py +398 -132
  15. valetudo_map_parser/hypfer_handler.py +259 -241
  16. valetudo_map_parser/hypfer_rooms_handler.py +599 -0
  17. valetudo_map_parser/map_data.py +45 -64
  18. valetudo_map_parser/rand25_handler.py +429 -310
  19. valetudo_map_parser/reimg_draw.py +55 -74
  20. valetudo_map_parser/rooms_handler.py +470 -0
  21. valetudo_map_parser-0.1.9a2.dist-info/METADATA +93 -0
  22. valetudo_map_parser-0.1.9a2.dist-info/RECORD +27 -0
  23. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/WHEEL +1 -1
  24. valetudo_map_parser/images_utils.py +0 -398
  25. valetudo_map_parser-0.1.8.dist-info/METADATA +0 -23
  26. valetudo_map_parser-0.1.8.dist-info/RECORD +0 -20
  27. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/LICENSE +0 -0
  28. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/NOTICE.txt +0 -0
@@ -3,12 +3,141 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import StrEnum
6
- from typing import List, Dict, Tuple
7
- import logging
6
+ from typing import Dict, List, Tuple
8
7
 
9
- _LOGGER = logging.getLogger(__name__)
8
+ import numpy as np
9
+ from scipy import ndimage
10
10
 
11
- Color = Tuple[int, int, int, int] # RGBA color definition
11
+ from .types import (
12
+ ALPHA_BACKGROUND,
13
+ ALPHA_CHARGER,
14
+ ALPHA_GO_TO,
15
+ ALPHA_MOVE,
16
+ ALPHA_NO_GO,
17
+ ALPHA_ROBOT,
18
+ ALPHA_ROOM_0,
19
+ ALPHA_ROOM_1,
20
+ ALPHA_ROOM_2,
21
+ ALPHA_ROOM_3,
22
+ ALPHA_ROOM_4,
23
+ ALPHA_ROOM_5,
24
+ ALPHA_ROOM_6,
25
+ ALPHA_ROOM_7,
26
+ ALPHA_ROOM_8,
27
+ ALPHA_ROOM_9,
28
+ ALPHA_ROOM_10,
29
+ ALPHA_ROOM_11,
30
+ ALPHA_ROOM_12,
31
+ ALPHA_ROOM_13,
32
+ ALPHA_ROOM_14,
33
+ ALPHA_ROOM_15,
34
+ ALPHA_TEXT,
35
+ ALPHA_WALL,
36
+ ALPHA_ZONE_CLEAN,
37
+ COLOR_BACKGROUND,
38
+ COLOR_CHARGER,
39
+ COLOR_GO_TO,
40
+ COLOR_MOVE,
41
+ COLOR_NO_GO,
42
+ COLOR_ROBOT,
43
+ COLOR_ROOM_0,
44
+ COLOR_ROOM_1,
45
+ COLOR_ROOM_2,
46
+ COLOR_ROOM_3,
47
+ COLOR_ROOM_4,
48
+ COLOR_ROOM_5,
49
+ COLOR_ROOM_6,
50
+ COLOR_ROOM_7,
51
+ COLOR_ROOM_8,
52
+ COLOR_ROOM_9,
53
+ COLOR_ROOM_10,
54
+ COLOR_ROOM_11,
55
+ COLOR_ROOM_12,
56
+ COLOR_ROOM_13,
57
+ COLOR_ROOM_14,
58
+ COLOR_ROOM_15,
59
+ COLOR_TEXT,
60
+ COLOR_WALL,
61
+ COLOR_ZONE_CLEAN,
62
+ LOGGER,
63
+ Color,
64
+ )
65
+
66
+
67
+ color_transparent = (0, 0, 0, 0)
68
+ color_charger = (0, 128, 0, 255)
69
+ color_move = (238, 247, 255, 255)
70
+ color_robot = (255, 255, 204, 255)
71
+ color_no_go = (255, 0, 0, 255)
72
+ color_go_to = (0, 255, 0, 255)
73
+ color_background = (0, 125, 255, 255)
74
+ color_zone_clean = (255, 255, 255, 125)
75
+ color_wall = (255, 255, 0, 255)
76
+ color_text = (255, 255, 255, 255)
77
+ color_grey = (125, 125, 125, 255)
78
+ color_black = (0, 0, 0, 255)
79
+ color_room_0 = (135, 206, 250, 255)
80
+ color_room_1 = (176, 226, 255, 255)
81
+ color_room_2 = (164, 211, 238, 255)
82
+ color_room_3 = (141, 182, 205, 255)
83
+ color_room_4 = (96, 123, 139, 255)
84
+ color_room_5 = (224, 255, 255, 255)
85
+ color_room_6 = (209, 238, 238, 255)
86
+ color_room_7 = (180, 205, 205, 255)
87
+ color_room_8 = (122, 139, 139, 255)
88
+ color_room_9 = (175, 238, 238, 255)
89
+ color_room_10 = (84, 153, 199, 255)
90
+ color_room_11 = (133, 193, 233, 255)
91
+ color_room_12 = (245, 176, 65, 255)
92
+ color_room_13 = (82, 190, 128, 255)
93
+ color_room_14 = (72, 201, 176, 255)
94
+ color_room_15 = (165, 105, 18, 255)
95
+
96
+ rooms_color = [
97
+ color_room_0,
98
+ color_room_1,
99
+ color_room_2,
100
+ color_room_3,
101
+ color_room_4,
102
+ color_room_5,
103
+ color_room_6,
104
+ color_room_7,
105
+ color_room_8,
106
+ color_room_9,
107
+ color_room_10,
108
+ color_room_11,
109
+ color_room_12,
110
+ color_room_13,
111
+ color_room_14,
112
+ color_room_15,
113
+ ]
114
+
115
+ base_colors_array = [
116
+ color_wall,
117
+ color_zone_clean,
118
+ color_robot,
119
+ color_background,
120
+ color_move,
121
+ color_charger,
122
+ color_no_go,
123
+ color_go_to,
124
+ color_text,
125
+ ]
126
+
127
+ color_array = [
128
+ base_colors_array[0], # color_wall
129
+ base_colors_array[6], # color_no_go
130
+ base_colors_array[7], # color_go_to
131
+ color_black,
132
+ base_colors_array[2], # color_robot
133
+ base_colors_array[5], # color_charger
134
+ color_text,
135
+ base_colors_array[4], # color_move
136
+ base_colors_array[3], # color_background
137
+ base_colors_array[1], # color_zone_clean
138
+ color_transparent,
139
+ rooms_color,
140
+ ]
12
141
 
13
142
 
14
143
  class SupportedColor(StrEnum):
@@ -37,7 +166,7 @@ class DefaultColors:
37
166
 
38
167
  COLORS_RGB: Dict[str, Tuple[int, int, int]] = {
39
168
  SupportedColor.CHARGER: (255, 128, 0),
40
- SupportedColor.PATH: (238, 247, 255),
169
+ SupportedColor.PATH: (50, 150, 255), # More vibrant blue for better visibility
41
170
  SupportedColor.PREDICTED_PATH: (93, 109, 126),
42
171
  SupportedColor.WALLS: (255, 255, 0),
43
172
  SupportedColor.ROBOT: (255, 255, 204),
@@ -76,6 +205,13 @@ class DefaultColors:
76
205
  DEFAULT_ALPHA: Dict[str, float] = {
77
206
  f"alpha_{key}": 255.0 for key in COLORS_RGB.keys()
78
207
  }
208
+ # Override specific alpha values
209
+ DEFAULT_ALPHA.update(
210
+ {
211
+ "alpha_color_path": 200.0, # Make path slightly transparent but still very visible
212
+ "alpha_color_wall": 150.0, # Keep walls semi-transparent
213
+ }
214
+ )
79
215
  DEFAULT_ALPHA.update({f"alpha_room_{i}": 255.0 for i in range(16)})
80
216
 
81
217
  @classmethod
@@ -85,16 +221,143 @@ class DefaultColors:
85
221
  return r, g, b, int(alpha)
86
222
 
87
223
 
88
- class ColorsManagment:
224
+ class ColorsManagement:
89
225
  """Manages user-defined and default colors for map elements."""
90
226
 
91
- def __init__(self, device_info: dict) -> None:
227
+ def __init__(self, shared_var) -> None:
228
+ """
229
+ Initialize ColorsManagement for Home Assistant.
230
+ Uses optimized initialization for better performance.
231
+ """
232
+ self.shared_var = shared_var
233
+ self.color_cache = {} # Cache for frequently used color blends
234
+
235
+ # Initialize colors efficiently
236
+ self.user_colors = self.initialize_user_colors(self.shared_var.device_info)
237
+ self.rooms_colors = self.initialize_rooms_colors(self.shared_var.device_info)
238
+
239
+ @staticmethod
240
+ def add_alpha_to_rgb(alpha_channels, rgb_colors):
92
241
  """
93
- Initialize ColorsManagment with optional device_info from Home Assistant.
94
- :param device_info: Dictionary containing user-defined RGB colors and alpha values.
242
+ Add alpha channel to RGB colors using corresponding alpha channels.
243
+ Uses NumPy for vectorized operations when possible for better performance.
244
+
245
+ Args:
246
+ alpha_channels (List[Optional[float]]): List of alpha channel values (0.0-255.0).
247
+ rgb_colors (List[Tuple[int, int, int]]): List of RGB colors.
248
+
249
+ Returns:
250
+ List[Tuple[int, int, int, int]]: List of RGBA colors with alpha channel added.
95
251
  """
96
- self.user_colors = self.initialize_user_colors(device_info)
97
- self.rooms_colors = self.initialize_rooms_colors(device_info)
252
+ if len(alpha_channels) != len(rgb_colors):
253
+ LOGGER.error("Input lists must have the same length.")
254
+ return []
255
+
256
+ # Fast path for empty lists
257
+ if not rgb_colors:
258
+ return []
259
+
260
+ # Try to use NumPy for vectorized operations
261
+ try:
262
+ # Convert inputs to NumPy arrays for vectorized processing
263
+ alphas = np.array(alpha_channels, dtype=np.float32)
264
+
265
+ # Clip alpha values to valid range [0, 255]
266
+ alphas = np.clip(alphas, 0, 255).astype(np.int32)
267
+
268
+ # Process RGB colors
269
+ result = []
270
+ for _, (alpha, rgb) in enumerate(zip(alphas, rgb_colors)):
271
+ if rgb is None:
272
+ result.append((0, 0, 0, int(alpha)))
273
+ else:
274
+ result.append((rgb[0], rgb[1], rgb[2], int(alpha)))
275
+
276
+ return result
277
+
278
+ except (ValueError, TypeError, AttributeError):
279
+ # Fallback to non-vectorized method if NumPy processing fails
280
+ result = []
281
+ for alpha, rgb in zip(alpha_channels, rgb_colors):
282
+ try:
283
+ alpha_int = int(alpha)
284
+ alpha_int = max(0, min(255, alpha_int)) # Clip to valid range
285
+
286
+ if rgb is None:
287
+ result.append((0, 0, 0, alpha_int))
288
+ else:
289
+ result.append((rgb[0], rgb[1], rgb[2], alpha_int))
290
+ except (ValueError, TypeError):
291
+ result.append(None)
292
+
293
+ return result
294
+
295
+ def set_initial_colours(self, device_info: dict) -> None:
296
+ """Set the initial colours for the map using optimized methods."""
297
+ try:
298
+ # Define color keys and default values
299
+ base_color_keys = [
300
+ (COLOR_WALL, color_wall, ALPHA_WALL),
301
+ (COLOR_ZONE_CLEAN, color_zone_clean, ALPHA_ZONE_CLEAN),
302
+ (COLOR_ROBOT, color_robot, ALPHA_ROBOT),
303
+ (COLOR_BACKGROUND, color_background, ALPHA_BACKGROUND),
304
+ (COLOR_MOVE, color_move, ALPHA_MOVE),
305
+ (COLOR_CHARGER, color_charger, ALPHA_CHARGER),
306
+ (COLOR_NO_GO, color_no_go, ALPHA_NO_GO),
307
+ (COLOR_GO_TO, color_go_to, ALPHA_GO_TO),
308
+ (COLOR_TEXT, color_text, ALPHA_TEXT),
309
+ ]
310
+
311
+ room_color_keys = [
312
+ (COLOR_ROOM_0, color_room_0, ALPHA_ROOM_0),
313
+ (COLOR_ROOM_1, color_room_1, ALPHA_ROOM_1),
314
+ (COLOR_ROOM_2, color_room_2, ALPHA_ROOM_2),
315
+ (COLOR_ROOM_3, color_room_3, ALPHA_ROOM_3),
316
+ (COLOR_ROOM_4, color_room_4, ALPHA_ROOM_4),
317
+ (COLOR_ROOM_5, color_room_5, ALPHA_ROOM_5),
318
+ (COLOR_ROOM_6, color_room_6, ALPHA_ROOM_6),
319
+ (COLOR_ROOM_7, color_room_7, ALPHA_ROOM_7),
320
+ (COLOR_ROOM_8, color_room_8, ALPHA_ROOM_8),
321
+ (COLOR_ROOM_9, color_room_9, ALPHA_ROOM_9),
322
+ (COLOR_ROOM_10, color_room_10, ALPHA_ROOM_10),
323
+ (COLOR_ROOM_11, color_room_11, ALPHA_ROOM_11),
324
+ (COLOR_ROOM_12, color_room_12, ALPHA_ROOM_12),
325
+ (COLOR_ROOM_13, color_room_13, ALPHA_ROOM_13),
326
+ (COLOR_ROOM_14, color_room_14, ALPHA_ROOM_14),
327
+ (COLOR_ROOM_15, color_room_15, ALPHA_ROOM_15),
328
+ ]
329
+
330
+ # Extract user colors and alphas efficiently
331
+ user_colors = [
332
+ device_info.get(color_key, default_color)
333
+ for color_key, default_color, _ in base_color_keys
334
+ ]
335
+ user_alpha = [
336
+ device_info.get(alpha_key, 255) for _, _, alpha_key in base_color_keys
337
+ ]
338
+
339
+ # Extract room colors and alphas efficiently
340
+ rooms_colors = [
341
+ device_info.get(color_key, default_color)
342
+ for color_key, default_color, _ in room_color_keys
343
+ ]
344
+ rooms_alpha = [
345
+ device_info.get(alpha_key, 255) for _, _, alpha_key in room_color_keys
346
+ ]
347
+
348
+ # Use our optimized add_alpha_to_rgb method
349
+ self.shared_var.update_user_colors(
350
+ self.add_alpha_to_rgb(user_alpha, user_colors)
351
+ )
352
+ self.shared_var.update_rooms_colors(
353
+ self.add_alpha_to_rgb(rooms_alpha, rooms_colors)
354
+ )
355
+
356
+ # Clear the color cache after initialization
357
+ self.color_cache.clear()
358
+
359
+ except (ValueError, IndexError, UnboundLocalError) as e:
360
+ LOGGER.error("Error while populating colors: %s", e)
98
361
 
99
362
  def initialize_user_colors(self, device_info: dict) -> List[Color]:
100
363
  """
@@ -141,6 +404,120 @@ class ColorsManagment:
141
404
  """
142
405
  return (*rgb, int(alpha)) if rgb else (0, 0, 0, int(alpha))
143
406
 
407
+ @staticmethod
408
+ def blend_colors(background: Color, foreground: Color) -> Color:
409
+ """
410
+ Blend foreground color with background color based on alpha values.
411
+ Optimized version with more fast paths and simplified calculations.
412
+
413
+ :param background: Background RGBA color (r,g,b,a)
414
+ :param foreground: Foreground RGBA color (r,g,b,a) to blend on top
415
+ :return: Blended RGBA color
416
+ """
417
+ # Fast paths for common cases
418
+ fg_a = foreground[3]
419
+
420
+ if fg_a == 255: # Fully opaque foreground
421
+ return foreground
422
+
423
+ if fg_a == 0: # Fully transparent foreground
424
+ return background
425
+
426
+ bg_a = background[3]
427
+ if bg_a == 0: # Fully transparent background
428
+ return foreground
429
+
430
+ # Extract components (only after fast paths)
431
+ bg_r, bg_g, bg_b = background[:3]
432
+ fg_r, fg_g, fg_b = foreground[:3]
433
+
434
+ # Pre-calculate the blend factor once (avoid repeated division)
435
+ blend = fg_a / 255.0
436
+ inv_blend = 1.0 - blend
437
+
438
+ # Simple linear interpolation for RGB channels
439
+ # This is faster than the previous implementation
440
+ out_r = int(fg_r * blend + bg_r * inv_blend)
441
+ out_g = int(fg_g * blend + bg_g * inv_blend)
442
+ out_b = int(fg_b * blend + bg_b * inv_blend)
443
+
444
+ # Alpha blending - simplified calculation
445
+ out_a = int(fg_a + bg_a * inv_blend)
446
+
447
+ # No need for min/max checks as the blend math keeps values in range
448
+ # when input values are valid (0-255)
449
+
450
+ return [out_r, out_g, out_b, out_a]
451
+
452
+ # Cache for recently sampled background colors
453
+ _bg_color_cache = {}
454
+ _cache_size = 1024 # Limit cache size to avoid memory issues
455
+
456
+ @staticmethod
457
+ def sample_and_blend_color(array, x: int, y: int, foreground: Color) -> Color:
458
+ """
459
+ Sample the background color from the array at coordinates (x,y) and blend with foreground color.
460
+ Optimized version with caching and faster sampling.
461
+
462
+ Args:
463
+ array: The RGBA numpy array representing the image
464
+ x: Coordinate X to sample the background color from
465
+ y: Coordinate Y to sample the background color from
466
+ foreground: Foreground RGBA color (r,g,b,a) to blend on top
467
+
468
+ Returns:
469
+ Blended RGBA color
470
+ """
471
+ # Fast path for fully opaque foreground - no need to sample or blend
472
+ if foreground[3] == 255:
473
+ return foreground
474
+
475
+ # Ensure array exists
476
+ if array is None:
477
+ return foreground
478
+
479
+ # Check if coordinates are within bounds
480
+ height, width = array.shape[:2]
481
+ if not (0 <= y < height and 0 <= x < width):
482
+ return foreground
483
+
484
+ # Check cache for this coordinate
485
+ cache_key = (id(array), x, y)
486
+ cache = ColorsManagement._bg_color_cache
487
+
488
+ if cache_key in cache:
489
+ background = cache[cache_key]
490
+ else:
491
+ # Sample the background color using direct indexing (fastest method)
492
+ try:
493
+ background = tuple(map(int, array[y, x]))
494
+
495
+ # Update cache (with simple LRU-like behavior)
496
+ try:
497
+ if len(cache) >= ColorsManagement._cache_size:
498
+ # Remove a random entry if cache is full
499
+ if cache: # Make sure cache is not empty
500
+ cache.pop(next(iter(cache)))
501
+ else:
502
+ # If cache is somehow empty but len reported >= _cache_size
503
+ # This is an edge case that shouldn't happen but we handle it
504
+ pass
505
+ cache[cache_key] = background
506
+ except KeyError:
507
+ # If we encounter a KeyError, reset the cache
508
+ # This is a rare edge case that might happen in concurrent access
509
+ ColorsManagement._bg_color_cache = {cache_key: background}
510
+
511
+ except (IndexError, ValueError):
512
+ return foreground
513
+
514
+ # Fast path for fully transparent foreground
515
+ if foreground[3] == 0:
516
+ return background
517
+
518
+ # Blend the colors
519
+ return ColorsManagement.blend_colors(background, foreground)
520
+
144
521
  def get_user_colors(self) -> List[Color]:
145
522
  """Return the list of RGBA colors for user-defined map elements."""
146
523
  return self.user_colors
@@ -149,6 +526,278 @@ class ColorsManagment:
149
526
  """Return the list of RGBA colors for rooms."""
150
527
  return self.rooms_colors
151
528
 
529
+ @staticmethod
530
+ def batch_blend_colors(image_array, mask, foreground_color):
531
+ """
532
+ Blend a foreground color with all pixels in an image where the mask is True.
533
+ Uses scipy.ndimage for efficient batch processing.
534
+
535
+ Args:
536
+ image_array: NumPy array of shape (height, width, 4) containing RGBA image data
537
+ mask: Boolean mask of shape (height, width) indicating pixels to blend
538
+ foreground_color: RGBA color tuple to blend with the masked pixels
539
+
540
+ Returns:
541
+ Modified image array with blended colors
542
+ """
543
+ if not np.any(mask):
544
+ return image_array # No pixels to blend
545
+
546
+ # Extract foreground components
547
+ fg_r, fg_g, fg_b, fg_a = foreground_color
548
+
549
+ # Fast path for fully opaque foreground
550
+ if fg_a == 255:
551
+ # Just set the color directly where mask is True
552
+ image_array[mask, 0] = fg_r
553
+ image_array[mask, 1] = fg_g
554
+ image_array[mask, 2] = fg_b
555
+ image_array[mask, 3] = fg_a
556
+ return image_array
557
+
558
+ # Fast path for fully transparent foreground
559
+ if fg_a == 0:
560
+ return image_array # No change needed
561
+
562
+ # For semi-transparent foreground, we need to blend
563
+ # Extract background components where mask is True
564
+ bg_pixels = image_array[mask]
565
+
566
+ # Convert alpha from [0-255] to [0-1] for calculations
567
+ fg_alpha = fg_a / 255.0
568
+ bg_alpha = bg_pixels[:, 3] / 255.0
569
+
570
+ # Calculate resulting alpha
571
+ out_alpha = fg_alpha + bg_alpha * (1 - fg_alpha)
572
+
573
+ # Calculate alpha ratios for blending
574
+ # Handle division by zero by setting ratio to 0 where out_alpha is near zero
575
+ alpha_ratio = np.zeros_like(out_alpha)
576
+ valid_alpha = out_alpha > 0.0001
577
+ alpha_ratio[valid_alpha] = fg_alpha / out_alpha[valid_alpha]
578
+ inv_alpha_ratio = 1.0 - alpha_ratio
579
+
580
+ # Calculate blended RGB components
581
+ out_r = np.clip(
582
+ (fg_r * alpha_ratio + bg_pixels[:, 0] * inv_alpha_ratio), 0, 255
583
+ ).astype(np.uint8)
584
+ out_g = np.clip(
585
+ (fg_g * alpha_ratio + bg_pixels[:, 1] * inv_alpha_ratio), 0, 255
586
+ ).astype(np.uint8)
587
+ out_b = np.clip(
588
+ (fg_b * alpha_ratio + bg_pixels[:, 2] * inv_alpha_ratio), 0, 255
589
+ ).astype(np.uint8)
590
+ out_a = np.clip((out_alpha * 255), 0, 255).astype(np.uint8)
591
+
592
+ # Update the image array with blended values
593
+ image_array[mask, 0] = out_r
594
+ image_array[mask, 1] = out_g
595
+ image_array[mask, 2] = out_b
596
+ image_array[mask, 3] = out_a
597
+
598
+ return image_array
599
+
600
+ @staticmethod
601
+ def process_regions_with_colors(image_array, regions_mask, colors):
602
+ """
603
+ Process multiple regions in an image with different colors using scipy.ndimage.
604
+ This is much faster than processing each region separately.
605
+
606
+ Args:
607
+ image_array: NumPy array of shape (height, width, 4) containing RGBA image data
608
+ regions_mask: NumPy array of shape (height, width) with integer labels for different regions
609
+ colors: List of RGBA color tuples corresponding to each region label
610
+
611
+ Returns:
612
+ Modified image array with all regions colored and blended
613
+ """
614
+ # Skip processing if no regions or colors
615
+ if regions_mask is None or not np.any(regions_mask) or not colors:
616
+ return image_array
617
+
618
+ # Get unique region labels (excluding 0 which is typically background)
619
+ unique_labels = np.unique(regions_mask)
620
+ unique_labels = unique_labels[unique_labels > 0] # Skip background (0)
621
+
622
+ if len(unique_labels) == 0:
623
+ return image_array # No regions to process
624
+
625
+ # Process each region with its corresponding color
626
+ for label in unique_labels:
627
+ if label <= len(colors):
628
+ # Create mask for this region
629
+ region_mask = regions_mask == label
630
+
631
+ # Get color for this region
632
+ color = colors[label - 1] if label - 1 < len(colors) else colors[0]
633
+
634
+ # Apply color to this region
635
+ image_array = ColorsManagement.batch_blend_colors(
636
+ image_array, region_mask, color
637
+ )
638
+
639
+ return image_array
640
+
641
+ @staticmethod
642
+ def apply_color_to_shapes(image_array, shapes, color, thickness=1):
643
+ """
644
+ Apply a color to multiple shapes (lines, circles, etc.) using scipy.ndimage.
645
+
646
+ Args:
647
+ image_array: NumPy array of shape (height, width, 4) containing RGBA image data
648
+ shapes: List of shape definitions (each a list of points or parameters)
649
+ color: RGBA color tuple to apply to the shapes
650
+ thickness: Line thickness for shapes
651
+
652
+ Returns:
653
+ Modified image array with shapes drawn and blended
654
+ """
655
+ height, width = image_array.shape[:2]
656
+
657
+ # Create a mask for all shapes
658
+ shapes_mask = np.zeros((height, width), dtype=bool)
659
+
660
+ # Draw all shapes into the mask
661
+ for shape in shapes:
662
+ if len(shape) >= 2: # At least two points for a line
663
+ # Draw line into mask
664
+ for i in range(len(shape) - 1):
665
+ x1, y1 = shape[i]
666
+ x2, y2 = shape[i + 1]
667
+
668
+ # Use Bresenham's line algorithm via scipy.ndimage.map_coordinates
669
+ # Create coordinates for the line
670
+ length = int(np.hypot(x2 - x1, y2 - y1))
671
+ if length == 0:
672
+ continue
673
+
674
+ t = np.linspace(0, 1, length * 2)
675
+ x = np.round(x1 * (1 - t) + x2 * t).astype(int)
676
+ y = np.round(y1 * (1 - t) + y2 * t).astype(int)
677
+
678
+ # Filter points outside the image
679
+ valid = (0 <= x) & (x < width) & (0 <= y) & (y < height)
680
+ x, y = x[valid], y[valid]
681
+
682
+ # Add points to mask
683
+ if thickness == 1:
684
+ shapes_mask[y, x] = True
685
+ else:
686
+ # For thicker lines, use a disk structuring element
687
+ # Create a disk structuring element once
688
+ disk_radius = thickness
689
+ disk_size = 2 * disk_radius + 1
690
+ disk_struct = np.zeros((disk_size, disk_size), dtype=bool)
691
+ y_grid, x_grid = np.ogrid[
692
+ -disk_radius : disk_radius + 1,
693
+ -disk_radius : disk_radius + 1,
694
+ ]
695
+ mask = x_grid**2 + y_grid**2 <= disk_radius**2
696
+ disk_struct[mask] = True
697
+
698
+ # Use scipy.ndimage.binary_dilation for efficient dilation
699
+ # Create a temporary mask for this line segment
700
+ line_mask = np.zeros_like(shapes_mask)
701
+ line_mask[y, x] = True
702
+ # Dilate the line with the disk structuring element
703
+ dilated_line = ndimage.binary_dilation(
704
+ line_mask, structure=disk_struct
705
+ )
706
+ # Add to the overall shapes mask
707
+ shapes_mask |= dilated_line
708
+
709
+ # Apply color to all shapes at once
710
+ return ColorsManagement.batch_blend_colors(image_array, shapes_mask, color)
711
+
712
+ @staticmethod
713
+ def batch_sample_colors(image_array, coordinates):
714
+ """
715
+ Efficiently sample colors from multiple coordinates in an image using scipy.ndimage.
716
+
717
+ Args:
718
+ image_array: NumPy array of shape (height, width, 4) containing RGBA image data
719
+ coordinates: List of (x,y) tuples or numpy array of shape (N,2) with coordinates to sample
720
+
721
+ Returns:
722
+ NumPy array of shape (N,4) containing the RGBA colors at each coordinate
723
+ """
724
+ if len(coordinates) == 0:
725
+ return np.array([])
726
+
727
+ height, width = image_array.shape[:2]
728
+
729
+ # Convert coordinates to numpy array if not already
730
+ coords = np.array(coordinates)
731
+
732
+ # Separate x and y coordinates
733
+ x_coords = coords[:, 0]
734
+ y_coords = coords[:, 1]
735
+
736
+ # Create a mask for valid coordinates (within image bounds)
737
+ valid_mask = (
738
+ (0 <= x_coords) & (x_coords < width) & (0 <= y_coords) & (y_coords < height)
739
+ )
740
+
741
+ # Initialize result array with zeros
742
+ result = np.zeros((len(coordinates), 4), dtype=np.uint8)
743
+
744
+ if not np.any(valid_mask):
745
+ return result # No valid coordinates
746
+
747
+ # Filter valid coordinates
748
+ valid_x = x_coords[valid_mask].astype(int)
749
+ valid_y = y_coords[valid_mask].astype(int)
750
+
751
+ # Use scipy.ndimage.map_coordinates for efficient sampling
752
+ # This is much faster than looping through coordinates
753
+ for channel in range(4):
754
+ # Sample this color channel for all valid coordinates at once
755
+ channel_values = ndimage.map_coordinates(
756
+ image_array[..., channel],
757
+ np.vstack((valid_y, valid_x)),
758
+ order=0, # Use nearest-neighbor interpolation
759
+ mode="nearest",
760
+ )
761
+
762
+ # Assign sampled values to result array
763
+ result[valid_mask, channel] = channel_values
764
+
765
+ return result
766
+
767
+ def cached_blend_colors(self, background: Color, foreground: Color) -> Color:
768
+ """
769
+ Cached version of blend_colors that stores frequently used combinations.
770
+ This improves performance when the same color combinations are used repeatedly.
771
+
772
+ Args:
773
+ background: Background RGBA color tuple
774
+ foreground: Foreground RGBA color tuple
775
+
776
+ Returns:
777
+ Blended RGBA color tuple
778
+ """
779
+ # Fast paths for common cases
780
+ if foreground[3] == 255:
781
+ return foreground
782
+ if foreground[3] == 0:
783
+ return background
784
+
785
+ # Create a cache key from the color tuples
786
+ cache_key = (background, foreground)
787
+
788
+ # Check if this combination is in the cache
789
+ if cache_key in self.color_cache:
790
+ return self.color_cache[cache_key]
791
+
792
+ # Calculate the blended color
793
+ result = ColorsManagement.blend_colors(background, foreground)
794
+
795
+ # Store in cache (with a maximum cache size to prevent memory issues)
796
+ if len(self.color_cache) < 1000: # Limit cache size
797
+ self.color_cache[cache_key] = result
798
+
799
+ return result
800
+
152
801
  def get_colour(self, supported_color: SupportedColor) -> Color:
153
802
  """
154
803
  Retrieve the color for a specific map element, prioritizing user-defined values.
@@ -162,7 +811,7 @@ class ColorsManagment:
162
811
  try:
163
812
  return self.rooms_colors[room_index]
164
813
  except (IndexError, KeyError):
165
- _LOGGER.warning("Room index %s not found, using default.", room_index)
814
+ LOGGER.warning("Room index %s not found, using default.", room_index)
166
815
  r, g, b = DefaultColors.DEFAULT_ROOM_COLORS[f"color_room_{room_index}"]
167
816
  a = DefaultColors.DEFAULT_ALPHA[f"alpha_room_{room_index}"]
168
817
  return r, g, b, int(a)
@@ -172,7 +821,7 @@ class ColorsManagment:
172
821
  index = list(SupportedColor).index(supported_color)
173
822
  return self.user_colors[index]
174
823
  except (IndexError, KeyError, ValueError):
175
- _LOGGER.warning(
824
+ LOGGER.warning(
176
825
  "Color for %s not found. Returning default.", supported_color
177
826
  )
178
827
  return DefaultColors.get_rgba(supported_color, 255) # Transparent fallback