valetudo-map-parser 0.1.11b2__py3-none-any.whl → 0.1.12b0__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 +124 -15
- valetudo_map_parser/config/async_utils.py +1 -1
- valetudo_map_parser/config/colors.py +2 -419
- valetudo_map_parser/config/drawable.py +10 -73
- valetudo_map_parser/config/rand256_parser.py +129 -111
- valetudo_map_parser/config/shared.py +15 -12
- valetudo_map_parser/config/status_text/__init__.py +6 -0
- valetudo_map_parser/config/status_text/status_text.py +74 -49
- valetudo_map_parser/config/types.py +74 -408
- valetudo_map_parser/config/utils.py +99 -73
- valetudo_map_parser/const.py +288 -0
- valetudo_map_parser/hypfer_draw.py +121 -150
- valetudo_map_parser/hypfer_handler.py +210 -183
- valetudo_map_parser/map_data.py +31 -0
- valetudo_map_parser/rand256_handler.py +207 -218
- valetudo_map_parser/rooms_handler.py +4 -5
- {valetudo_map_parser-0.1.11b2.dist-info → valetudo_map_parser-0.1.12b0.dist-info}/METADATA +1 -1
- valetudo_map_parser-0.1.12b0.dist-info/RECORD +34 -0
- valetudo_map_parser-0.1.11b2.dist-info/RECORD +0 -32
- {valetudo_map_parser-0.1.11b2.dist-info → valetudo_map_parser-0.1.12b0.dist-info}/WHEEL +0 -0
- {valetudo_map_parser-0.1.11b2.dist-info → valetudo_map_parser-0.1.12b0.dist-info}/licenses/LICENSE +0 -0
- {valetudo_map_parser-0.1.11b2.dist-info → valetudo_map_parser-0.1.12b0.dist-info}/licenses/NOTICE.txt +0 -0
valetudo_map_parser/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Valetudo map parser.
|
|
2
|
-
Version: 0.1.
|
|
2
|
+
Version: 0.1.12"""
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
@@ -22,6 +22,58 @@ from .config.types import (
|
|
|
22
22
|
TrimCropData,
|
|
23
23
|
UserLanguageStore,
|
|
24
24
|
)
|
|
25
|
+
from .config.utils import ResizeParams, async_resize_image
|
|
26
|
+
from .const import (
|
|
27
|
+
ATTR_CALIBRATION_POINTS,
|
|
28
|
+
ATTR_CAMERA_MODE,
|
|
29
|
+
ATTR_CONTENT_TYPE,
|
|
30
|
+
ATTR_FRIENDLY_NAME,
|
|
31
|
+
ATTR_IMAGE_LAST_UPDATED,
|
|
32
|
+
ATTR_JSON_DATA,
|
|
33
|
+
ATTR_OBSTACLES,
|
|
34
|
+
ATTR_POINTS,
|
|
35
|
+
ATTR_ROOMS,
|
|
36
|
+
ATTR_ROTATE,
|
|
37
|
+
ATTR_SNAPSHOT,
|
|
38
|
+
ATTR_SNAPSHOT_PATH,
|
|
39
|
+
ATTR_VACUUM_BATTERY,
|
|
40
|
+
ATTR_VACUUM_CHARGING,
|
|
41
|
+
ATTR_VACUUM_JSON_ID,
|
|
42
|
+
ATTR_VACUUM_POSITION,
|
|
43
|
+
ATTR_VACUUM_STATUS,
|
|
44
|
+
ATTR_VACUUM_TOPIC,
|
|
45
|
+
ATTR_ZONES,
|
|
46
|
+
CAMERA_STORAGE,
|
|
47
|
+
COLORS,
|
|
48
|
+
CONF_ASPECT_RATIO,
|
|
49
|
+
CONF_AUTO_ZOOM,
|
|
50
|
+
CONF_EXPORT_SVG,
|
|
51
|
+
CONF_OFFSET_BOTTOM,
|
|
52
|
+
CONF_OFFSET_LEFT,
|
|
53
|
+
CONF_OFFSET_RIGHT,
|
|
54
|
+
CONF_OFFSET_TOP,
|
|
55
|
+
CONF_SNAPSHOTS_ENABLE,
|
|
56
|
+
CONF_TRIMS_SAVE,
|
|
57
|
+
CONF_VACUUM_CONFIG_ENTRY_ID,
|
|
58
|
+
CONF_VACUUM_CONNECTION_STRING,
|
|
59
|
+
CONF_VACUUM_ENTITY_ID,
|
|
60
|
+
CONF_VACUUM_IDENTIFIERS,
|
|
61
|
+
CONF_VAC_STAT,
|
|
62
|
+
CONF_VAC_STAT_FONT,
|
|
63
|
+
CONF_VAC_STAT_POS,
|
|
64
|
+
CONF_VAC_STAT_SIZE,
|
|
65
|
+
CONF_ZOOM_LOCK_RATIO,
|
|
66
|
+
DECODED_TOPICS,
|
|
67
|
+
DEFAULT_IMAGE_SIZE,
|
|
68
|
+
DEFAULT_PIXEL_SIZE,
|
|
69
|
+
DEFAULT_VALUES,
|
|
70
|
+
FONTS_AVAILABLE,
|
|
71
|
+
ICON,
|
|
72
|
+
NAME,
|
|
73
|
+
NON_DECODED_TOPICS,
|
|
74
|
+
NOT_STREAMING_STATES,
|
|
75
|
+
SENSOR_NO_DATA,
|
|
76
|
+
)
|
|
25
77
|
from .hypfer_handler import HypferMapImageHandler
|
|
26
78
|
from .map_data import HyperMapData
|
|
27
79
|
from .rand256_handler import ReImageHandler
|
|
@@ -38,29 +90,86 @@ def get_default_font_path() -> str:
|
|
|
38
90
|
|
|
39
91
|
|
|
40
92
|
__all__ = [
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
93
|
+
# Attribute Constants
|
|
94
|
+
"ATTR_CALIBRATION_POINTS",
|
|
95
|
+
"ATTR_CAMERA_MODE",
|
|
96
|
+
"ATTR_CONTENT_TYPE",
|
|
97
|
+
"ATTR_FRIENDLY_NAME",
|
|
98
|
+
"ATTR_IMAGE_LAST_UPDATED",
|
|
99
|
+
"ATTR_JSON_DATA",
|
|
100
|
+
"ATTR_OBSTACLES",
|
|
101
|
+
"ATTR_POINTS",
|
|
102
|
+
"ATTR_ROOMS",
|
|
103
|
+
"ATTR_ROTATE",
|
|
104
|
+
"ATTR_SNAPSHOT",
|
|
105
|
+
"ATTR_SNAPSHOT_PATH",
|
|
106
|
+
"ATTR_VACUUM_BATTERY",
|
|
107
|
+
"ATTR_VACUUM_CHARGING",
|
|
108
|
+
"ATTR_VACUUM_JSON_ID",
|
|
109
|
+
"ATTR_VACUUM_POSITION",
|
|
110
|
+
"ATTR_VACUUM_STATUS",
|
|
111
|
+
"ATTR_VACUUM_TOPIC",
|
|
112
|
+
"ATTR_ZONES",
|
|
113
|
+
# Configuration Constants
|
|
114
|
+
"CAMERA_STORAGE",
|
|
115
|
+
"COLORS",
|
|
116
|
+
"CONF_ASPECT_RATIO",
|
|
117
|
+
"CONF_AUTO_ZOOM",
|
|
118
|
+
"CONF_EXPORT_SVG",
|
|
119
|
+
"CONF_OFFSET_BOTTOM",
|
|
120
|
+
"CONF_OFFSET_LEFT",
|
|
121
|
+
"CONF_OFFSET_RIGHT",
|
|
122
|
+
"CONF_OFFSET_TOP",
|
|
123
|
+
"CONF_SNAPSHOTS_ENABLE",
|
|
124
|
+
"CONF_TRIMS_SAVE",
|
|
125
|
+
"CONF_VACUUM_CONFIG_ENTRY_ID",
|
|
126
|
+
"CONF_VACUUM_CONNECTION_STRING",
|
|
127
|
+
"CONF_VACUUM_ENTITY_ID",
|
|
128
|
+
"CONF_VACUUM_IDENTIFIERS",
|
|
129
|
+
"CONF_VAC_STAT",
|
|
130
|
+
"CONF_VAC_STAT_FONT",
|
|
131
|
+
"CONF_VAC_STAT_POS",
|
|
132
|
+
"CONF_VAC_STAT_SIZE",
|
|
133
|
+
"CONF_ZOOM_LOCK_RATIO",
|
|
134
|
+
# Default Values
|
|
135
|
+
"DECODED_TOPICS",
|
|
136
|
+
"DEFAULT_IMAGE_SIZE",
|
|
137
|
+
"DEFAULT_PIXEL_SIZE",
|
|
138
|
+
"DEFAULT_VALUES",
|
|
139
|
+
"FONTS_AVAILABLE",
|
|
140
|
+
"ICON",
|
|
141
|
+
"NAME",
|
|
142
|
+
"NON_DECODED_TOPICS",
|
|
143
|
+
"NOT_STREAMING_STATES",
|
|
144
|
+
"SENSOR_NO_DATA",
|
|
145
|
+
# Classes and Handlers
|
|
47
146
|
"CameraShared",
|
|
48
147
|
"CameraSharedManager",
|
|
49
148
|
"ColorsManagement",
|
|
50
149
|
"Drawable",
|
|
51
150
|
"DrawableElement",
|
|
52
151
|
"DrawingConfig",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
152
|
+
"HyperMapData",
|
|
153
|
+
"HypferMapImageHandler",
|
|
154
|
+
"RRMapParser",
|
|
155
|
+
"RandRoomsHandler",
|
|
156
|
+
"ReImageHandler",
|
|
157
|
+
"RoomsHandler",
|
|
158
|
+
"StatusText",
|
|
159
|
+
# Types
|
|
58
160
|
"CameraModes",
|
|
161
|
+
"ImageSize",
|
|
59
162
|
"JsonType",
|
|
60
|
-
"PilPNG",
|
|
61
163
|
"NumpyArray",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
164
|
+
"PilPNG",
|
|
165
|
+
"RoomsProperties",
|
|
166
|
+
"RoomStore",
|
|
167
|
+
"SnapshotStore",
|
|
168
|
+
"TrimCropData",
|
|
169
|
+
"UserLanguageStore",
|
|
170
|
+
# Utilities
|
|
171
|
+
"ResizeParams",
|
|
64
172
|
"STATUS_TEXT_TRANSLATIONS",
|
|
173
|
+
"async_resize_image",
|
|
65
174
|
"get_default_font_path",
|
|
66
175
|
]
|
|
@@ -6,9 +6,8 @@ from enum import StrEnum
|
|
|
6
6
|
from typing import Dict, List, Tuple
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
|
-
from scipy import ndimage
|
|
10
9
|
|
|
11
|
-
from
|
|
10
|
+
from ..const import (
|
|
12
11
|
ALPHA_BACKGROUND,
|
|
13
12
|
ALPHA_CHARGER,
|
|
14
13
|
ALPHA_GO_TO,
|
|
@@ -59,10 +58,8 @@ from .types import (
|
|
|
59
58
|
COLOR_TEXT,
|
|
60
59
|
COLOR_WALL,
|
|
61
60
|
COLOR_ZONE_CLEAN,
|
|
62
|
-
LOGGER,
|
|
63
|
-
Color,
|
|
64
61
|
)
|
|
65
|
-
|
|
62
|
+
from .types import LOGGER, Color
|
|
66
63
|
|
|
67
64
|
color_transparent = (0, 0, 0, 0)
|
|
68
65
|
color_charger = (0, 128, 0, 255)
|
|
@@ -404,120 +401,6 @@ class ColorsManagement:
|
|
|
404
401
|
"""
|
|
405
402
|
return (*rgb, int(alpha)) if rgb else (0, 0, 0, int(alpha))
|
|
406
403
|
|
|
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
|
-
|
|
521
404
|
def get_user_colors(self) -> List[Color]:
|
|
522
405
|
"""Return the list of RGBA colors for user-defined map elements."""
|
|
523
406
|
return self.user_colors
|
|
@@ -525,303 +408,3 @@ class ColorsManagement:
|
|
|
525
408
|
def get_rooms_colors(self) -> List[Color]:
|
|
526
409
|
"""Return the list of RGBA colors for rooms."""
|
|
527
410
|
return self.rooms_colors
|
|
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
|
-
|
|
801
|
-
def get_colour(self, supported_color: SupportedColor) -> Color:
|
|
802
|
-
"""
|
|
803
|
-
Retrieve the color for a specific map element, prioritizing user-defined values.
|
|
804
|
-
|
|
805
|
-
:param supported_color: The SupportedColor key for the desired color.
|
|
806
|
-
:return: The RGBA color for the given map element.
|
|
807
|
-
"""
|
|
808
|
-
# Handle room-specific colors
|
|
809
|
-
if supported_color.startswith("color_room_"):
|
|
810
|
-
room_index = int(supported_color.split("_")[-1])
|
|
811
|
-
try:
|
|
812
|
-
return self.rooms_colors[room_index]
|
|
813
|
-
except (IndexError, KeyError):
|
|
814
|
-
LOGGER.warning("Room index %s not found, using default.", room_index)
|
|
815
|
-
r, g, b = DefaultColors.DEFAULT_ROOM_COLORS[f"color_room_{room_index}"]
|
|
816
|
-
a = DefaultColors.DEFAULT_ALPHA[f"alpha_room_{room_index}"]
|
|
817
|
-
return r, g, b, int(a)
|
|
818
|
-
|
|
819
|
-
# Handle general map element colors
|
|
820
|
-
try:
|
|
821
|
-
index = list(SupportedColor).index(supported_color)
|
|
822
|
-
return self.user_colors[index]
|
|
823
|
-
except (IndexError, KeyError, ValueError):
|
|
824
|
-
LOGGER.warning(
|
|
825
|
-
"Color for %s not found. Returning default.", supported_color
|
|
826
|
-
)
|
|
827
|
-
return DefaultColors.get_rgba(supported_color, 255) # Transparent fallback
|