valetudo-map-parser 0.1.7__py3-none-any.whl → 0.1.9a1__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.
- valetudo_map_parser/__init__.py +19 -12
- valetudo_map_parser/config/auto_crop.py +174 -116
- valetudo_map_parser/config/color_utils.py +105 -0
- valetudo_map_parser/config/colors.py +662 -13
- valetudo_map_parser/config/drawable.py +624 -279
- valetudo_map_parser/config/drawable_elements.py +292 -0
- valetudo_map_parser/config/enhanced_drawable.py +324 -0
- valetudo_map_parser/config/optimized_element_map.py +406 -0
- valetudo_map_parser/config/rand25_parser.py +42 -28
- valetudo_map_parser/config/room_outline.py +148 -0
- valetudo_map_parser/config/shared.py +29 -5
- valetudo_map_parser/config/types.py +102 -51
- valetudo_map_parser/config/utils.py +841 -0
- valetudo_map_parser/hypfer_draw.py +398 -132
- valetudo_map_parser/hypfer_handler.py +259 -241
- valetudo_map_parser/hypfer_rooms_handler.py +599 -0
- valetudo_map_parser/map_data.py +45 -64
- valetudo_map_parser/rand25_handler.py +429 -310
- valetudo_map_parser/reimg_draw.py +55 -74
- valetudo_map_parser/rooms_handler.py +470 -0
- valetudo_map_parser-0.1.9a1.dist-info/METADATA +93 -0
- valetudo_map_parser-0.1.9a1.dist-info/RECORD +27 -0
- {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9a1.dist-info}/WHEEL +1 -1
- valetudo_map_parser/images_utils.py +0 -398
- valetudo_map_parser-0.1.7.dist-info/METADATA +0 -23
- valetudo_map_parser-0.1.7.dist-info/RECORD +0 -20
- {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9a1.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9a1.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
|
7
|
-
import logging
|
6
|
+
from typing import Dict, List, Tuple
|
8
7
|
|
9
|
-
|
8
|
+
import numpy as np
|
9
|
+
from scipy import ndimage
|
10
10
|
|
11
|
-
|
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: (
|
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
|
224
|
+
class ColorsManagement:
|
89
225
|
"""Manages user-defined and default colors for map elements."""
|
90
226
|
|
91
|
-
def __init__(self,
|
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
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
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
|