valetudo-map-parser 0.1.9b43__py3-none-any.whl → 0.1.9b45__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/config/colors.py +88 -8
- valetudo_map_parser/config/drawable.py +83 -5
- valetudo_map_parser/config/drawable_elements.py +591 -16
- valetudo_map_parser/config/enhanced_drawable.py +4 -9
- valetudo_map_parser/config/optimized_element_map.py +363 -0
- valetudo_map_parser/config/room_outline.py +148 -0
- valetudo_map_parser/config/shared.py +1 -0
- valetudo_map_parser/config/utils.py +82 -65
- valetudo_map_parser/hypfer_draw.py +30 -32
- valetudo_map_parser/hypfer_handler.py +88 -100
- valetudo_map_parser/map_data.py +0 -9
- valetudo_map_parser/rand25_handler.py +173 -132
- valetudo_map_parser/utils/__init__.py +5 -0
- valetudo_map_parser/utils/color_utils.py +62 -0
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b45.dist-info}/METADATA +2 -1
- valetudo_map_parser-0.1.9b45.dist-info/RECORD +27 -0
- valetudo_map_parser-0.1.9b43.dist-info/RECORD +0 -23
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b45.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b45.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b45.dist-info}/WHEEL +0 -0
@@ -8,25 +8,36 @@ Version: 0.1.9
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
10
|
import json
|
11
|
-
import logging
|
12
11
|
|
13
12
|
import numpy as np
|
14
13
|
from PIL import Image
|
15
14
|
|
16
15
|
from .config.auto_crop import AutoCrop
|
17
|
-
from .config.
|
18
|
-
from .config.
|
19
|
-
from .config.enhanced_drawable import EnhancedDrawable
|
16
|
+
from .config.drawable_elements import DrawableElement
|
17
|
+
from .config.optimized_element_map import OptimizedElementMapGenerator
|
20
18
|
from .config.shared import CameraShared
|
21
|
-
from .config.types import
|
22
|
-
|
19
|
+
from .config.types import (
|
20
|
+
COLORS,
|
21
|
+
LOGGER,
|
22
|
+
CalibrationPoints,
|
23
|
+
Colors,
|
24
|
+
RoomsProperties,
|
25
|
+
RoomStore,
|
26
|
+
)
|
27
|
+
from .config.utils import (
|
28
|
+
BaseHandler,
|
29
|
+
get_element_at_position,
|
30
|
+
get_room_at_position,
|
31
|
+
initialize_drawing_config,
|
32
|
+
manage_drawable_elements,
|
33
|
+
prepare_resize_params,
|
34
|
+
update_element_map_with_robot,
|
35
|
+
)
|
36
|
+
from .config.room_outline import extract_room_outline_with_scipy
|
23
37
|
from .hypfer_draw import ImageDraw as ImDraw
|
24
38
|
from .map_data import ImageData
|
25
39
|
|
26
40
|
|
27
|
-
_LOGGER = logging.getLogger(__name__)
|
28
|
-
|
29
|
-
|
30
41
|
class HypferMapImageHandler(BaseHandler, AutoCrop):
|
31
42
|
"""Map Image Handler Class.
|
32
43
|
This class is used to handle the image data and the drawing of the map."""
|
@@ -40,8 +51,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
40
51
|
self.data = ImageData # imported Image Data Module.
|
41
52
|
|
42
53
|
# Initialize drawing configuration using the shared utility function
|
43
|
-
|
44
|
-
|
54
|
+
self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
|
55
|
+
self
|
56
|
+
)
|
45
57
|
|
46
58
|
self.go_to = None # vacuum go to data
|
47
59
|
self.img_hash = None # hash of the image calculated to check differences.
|
@@ -51,13 +63,15 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
51
63
|
self.imd = ImDraw(self) # Image Draw class.
|
52
64
|
self.color_grey = (128, 128, 128, 255)
|
53
65
|
self.file_name = self.shared.file_name # file name of the vacuum.
|
54
|
-
self.
|
66
|
+
self.element_map_manager = OptimizedElementMapGenerator(self.drawing_config,
|
67
|
+
self.shared) # Map of element codes
|
55
68
|
|
56
69
|
@staticmethod
|
57
70
|
def get_corners(x_max, x_min, y_max, y_min):
|
58
71
|
"""Get the corners of the room."""
|
59
72
|
return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
60
73
|
|
74
|
+
|
61
75
|
async def extract_room_outline_from_map(self, room_id_int, pixels, pixel_size):
|
62
76
|
"""Extract the outline of a room using the pixel data and element map.
|
63
77
|
|
@@ -84,13 +98,13 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
84
98
|
min_y, max_y = min(y_values), max(y_values)
|
85
99
|
|
86
100
|
# If we don't have an element map, return a rectangular outline
|
87
|
-
if not hasattr(self, "element_map") or self.element_map is None:
|
101
|
+
if not hasattr(self, "element_map") or self.shared.element_map is None:
|
88
102
|
# Return rectangular outline
|
89
103
|
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
90
104
|
|
91
105
|
# Create a binary mask for this room using the pixel data
|
92
106
|
# This is more reliable than using the element_map since we're directly using the pixel data
|
93
|
-
height, width = self.element_map.shape
|
107
|
+
height, width = self.shared.element_map.shape
|
94
108
|
room_mask = np.zeros((height, width), dtype=np.uint8)
|
95
109
|
|
96
110
|
# Fill the mask with room pixels using the pixel data
|
@@ -108,15 +122,16 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
108
122
|
|
109
123
|
# Debug log to check if we have any room pixels
|
110
124
|
num_room_pixels = np.sum(room_mask)
|
111
|
-
|
125
|
+
LOGGER.debug(
|
112
126
|
"%s: Room %s mask has %d pixels",
|
113
|
-
self.file_name,
|
127
|
+
self.file_name,
|
128
|
+
str(room_id_int),
|
129
|
+
int(num_room_pixels),
|
114
130
|
)
|
115
131
|
|
116
|
-
# Use the
|
117
|
-
|
118
|
-
|
119
|
-
room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int, _LOGGER
|
132
|
+
# Use the scipy-based room outline extraction
|
133
|
+
return await extract_room_outline_with_scipy(
|
134
|
+
room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int
|
120
135
|
)
|
121
136
|
|
122
137
|
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
@@ -141,27 +156,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
141
156
|
x_max,
|
142
157
|
y_max,
|
143
158
|
) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
|
144
|
-
|
145
|
-
# Get rectangular corners as a fallback
|
146
159
|
corners = self.get_corners(x_max, x_min, y_max, y_min)
|
147
|
-
|
148
|
-
# Try to extract a more accurate room outline from the element map
|
149
|
-
try:
|
150
|
-
# Extract the room outline using the element map
|
151
|
-
outline = await self.extract_room_outline_from_map(
|
152
|
-
segment_id, pixels, pixel_size
|
153
|
-
)
|
154
|
-
_LOGGER.debug(
|
155
|
-
"%s: Traced outline for room %s with %d points",
|
156
|
-
self.file_name,
|
157
|
-
segment_id,
|
158
|
-
len(outline),
|
159
|
-
)
|
160
|
-
except (ValueError, IndexError, TypeError, ArithmeticError) as e:
|
161
|
-
from .config.utils import handle_room_outline_error
|
162
|
-
handle_room_outline_error(self.file_name, segment_id, e, _LOGGER)
|
163
|
-
outline = corners
|
164
|
-
|
165
160
|
room_id = str(segment_id)
|
166
161
|
self.rooms_pos.append(
|
167
162
|
{
|
@@ -171,18 +166,18 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
171
166
|
)
|
172
167
|
room_properties[room_id] = {
|
173
168
|
"number": segment_id,
|
174
|
-
"outline":
|
169
|
+
"outline": corners,
|
175
170
|
"name": name,
|
176
171
|
"x": ((x_min + x_max) // 2),
|
177
172
|
"y": ((y_min + y_max) // 2),
|
178
173
|
}
|
179
174
|
if room_properties:
|
180
175
|
rooms = RoomStore(self.file_name, room_properties)
|
181
|
-
|
176
|
+
LOGGER.debug(
|
182
177
|
"%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
|
183
178
|
)
|
184
179
|
else:
|
185
|
-
|
180
|
+
LOGGER.debug("%s: Rooms data not available!", self.file_name)
|
186
181
|
self.rooms_pos = None
|
187
182
|
return room_properties
|
188
183
|
|
@@ -204,7 +199,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
204
199
|
# Check if the JSON data is not None else process the image.
|
205
200
|
try:
|
206
201
|
if m_json is not None:
|
207
|
-
|
202
|
+
LOGGER.debug("%s: Creating Image.", self.file_name)
|
208
203
|
# buffer json data
|
209
204
|
self.json_data = m_json
|
210
205
|
# Get the image size from the JSON data
|
@@ -237,11 +232,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
237
232
|
size_x, size_y, colors["background"]
|
238
233
|
)
|
239
234
|
|
240
|
-
# Create element map for tracking what's drawn where
|
241
|
-
self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
|
242
|
-
self.element_map[:] = DrawableElement.FLOOR
|
243
235
|
|
244
|
-
|
236
|
+
LOGGER.info("%s: Drawing map with color blending", self.file_name)
|
245
237
|
|
246
238
|
# Draw layers and segments if enabled
|
247
239
|
room_id = 0
|
@@ -268,7 +260,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
268
260
|
):
|
269
261
|
# Add this room to the disabled rooms set
|
270
262
|
disabled_rooms.add(room_id)
|
271
|
-
|
263
|
+
LOGGER.debug(
|
272
264
|
"%s: Room %d is disabled and will be skipped",
|
273
265
|
self.file_name,
|
274
266
|
current_room_id,
|
@@ -296,7 +288,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
296
288
|
)
|
297
289
|
if room_element:
|
298
290
|
# Log the room check for debugging
|
299
|
-
|
291
|
+
LOGGER.debug(
|
300
292
|
"%s: Checking if room %d is enabled: %s",
|
301
293
|
self.file_name,
|
302
294
|
current_room_id,
|
@@ -320,7 +312,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
320
312
|
if not self.drawing_config.is_enabled(
|
321
313
|
DrawableElement.WALL
|
322
314
|
):
|
323
|
-
|
315
|
+
LOGGER.info(
|
324
316
|
"%s: Skipping wall layer because WALL element is disabled",
|
325
317
|
self.file_name,
|
326
318
|
)
|
@@ -331,7 +323,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
331
323
|
# Need to modify compressed_pixels_list to exclude walls of disabled rooms
|
332
324
|
# This requires knowledge of which walls belong to which rooms
|
333
325
|
# For now, we'll just log that we're drawing walls for all rooms
|
334
|
-
|
326
|
+
LOGGER.debug(
|
335
327
|
"%s: Drawing walls for all rooms (including disabled ones)",
|
336
328
|
self.file_name,
|
337
329
|
)
|
@@ -390,7 +382,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
390
382
|
robot_y=(robot_position[1]),
|
391
383
|
angle=robot_position_angle,
|
392
384
|
)
|
393
|
-
|
385
|
+
LOGGER.info("%s: Completed base Layers", self.file_name)
|
394
386
|
# Copy the new array in base layer.
|
395
387
|
self.img_base_layer = await self.async_copy_array(img_np_array)
|
396
388
|
self.shared.frame_number = self.frame_number
|
@@ -399,7 +391,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
399
391
|
new_frame_hash != self.img_hash
|
400
392
|
):
|
401
393
|
self.frame_number = 0
|
402
|
-
|
394
|
+
LOGGER.debug(
|
403
395
|
"%s: %s at Frame Number: %s",
|
404
396
|
self.file_name,
|
405
397
|
str(self.json_id),
|
@@ -425,16 +417,16 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
425
417
|
|
426
418
|
# Draw path prediction and paths if enabled
|
427
419
|
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
428
|
-
|
420
|
+
LOGGER.info(
|
429
421
|
"%s: PATH element enabled: %s", self.file_name, path_enabled
|
430
422
|
)
|
431
423
|
if path_enabled:
|
432
|
-
|
424
|
+
LOGGER.info("%s: Drawing path", self.file_name)
|
433
425
|
img_np_array = await self.imd.async_draw_paths(
|
434
426
|
img_np_array, m_json, colors["move"], self.color_grey
|
435
427
|
)
|
436
428
|
else:
|
437
|
-
|
429
|
+
LOGGER.info("%s: Skipping path drawing", self.file_name)
|
438
430
|
|
439
431
|
# Check if the robot is docked.
|
440
432
|
if self.shared.vacuum_state == "docked":
|
@@ -459,8 +451,10 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
459
451
|
)
|
460
452
|
|
461
453
|
# Update element map for robot position
|
462
|
-
|
463
|
-
|
454
|
+
if hasattr(self.shared, 'element_map') and self.shared.element_map is not None:
|
455
|
+
update_element_map_with_robot(
|
456
|
+
self.shared.element_map, robot_position, DrawableElement.ROBOT
|
457
|
+
)
|
464
458
|
# Resize the image
|
465
459
|
img_np_array = await self.async_auto_trim_and_zoom_image(
|
466
460
|
img_np_array,
|
@@ -471,9 +465,24 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
471
465
|
)
|
472
466
|
# If the image is None return None and log the error.
|
473
467
|
if img_np_array is None:
|
474
|
-
|
468
|
+
LOGGER.warning("%s: Image array is None.", self.file_name)
|
475
469
|
return None
|
476
|
-
|
470
|
+
# Debug logging for element map creation
|
471
|
+
LOGGER.info("%s: Frame number: %d, has element_map: %s",
|
472
|
+
self.file_name, self.frame_number, hasattr(self.shared, 'element_map'))
|
473
|
+
|
474
|
+
if (self.shared.element_map is None) and (self.frame_number == 1):
|
475
|
+
# Create element map for tracking what's drawn where
|
476
|
+
LOGGER.info("%s: Creating element map with shape: %s",
|
477
|
+
self.file_name, img_np_array.shape)
|
478
|
+
|
479
|
+
# Generate the element map directly from JSON data
|
480
|
+
# This will create a cropped element map containing only the non-zero elements
|
481
|
+
LOGGER.info("%s: Generating element map from JSON data", self.file_name)
|
482
|
+
self.shared.element_map = await self.element_map_manager.async_generate_from_json(m_json)
|
483
|
+
|
484
|
+
LOGGER.info("%s: Element map created with shape: %s",
|
485
|
+
self.file_name, self.shared.element_map.shape if self.shared.element_map is not None else None)
|
477
486
|
# Convert the numpy array to a PIL image
|
478
487
|
pil_img = Image.fromarray(img_np_array, mode="RGBA")
|
479
488
|
del img_np_array
|
@@ -482,10 +491,10 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
482
491
|
resize_params = prepare_resize_params(self, pil_img, False)
|
483
492
|
resized_image = await self.async_resize_images(resize_params)
|
484
493
|
return resized_image
|
485
|
-
|
494
|
+
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
486
495
|
return pil_img
|
487
496
|
except (RuntimeError, RuntimeWarning) as e:
|
488
|
-
|
497
|
+
LOGGER.warning(
|
489
498
|
"%s: Error %s during image creation.",
|
490
499
|
self.file_name,
|
491
500
|
str(e),
|
@@ -499,12 +508,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
499
508
|
if self.room_propriety:
|
500
509
|
return self.room_propriety
|
501
510
|
if self.json_data:
|
502
|
-
|
511
|
+
LOGGER.debug("Checking %s Rooms data..", self.file_name)
|
503
512
|
self.room_propriety = await self.async_extract_room_properties(
|
504
513
|
self.json_data
|
505
514
|
)
|
506
515
|
if self.room_propriety:
|
507
|
-
|
516
|
+
LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
|
508
517
|
return self.room_propriety
|
509
518
|
|
510
519
|
def get_calibration_data(self) -> CalibrationPoints:
|
@@ -512,7 +521,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
512
521
|
this will create the attribute calibration points."""
|
513
522
|
calibration_data = []
|
514
523
|
rotation_angle = self.shared.image_rotate
|
515
|
-
|
524
|
+
LOGGER.info("Getting %s Calibrations points.", self.file_name)
|
516
525
|
|
517
526
|
# Define the map points (fixed)
|
518
527
|
map_points = self.get_map_points()
|
@@ -530,7 +539,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
530
539
|
def enable_element(self, element_code: DrawableElement) -> None:
|
531
540
|
"""Enable drawing of a specific element."""
|
532
541
|
self.drawing_config.enable_element(element_code)
|
533
|
-
|
542
|
+
LOGGER.info(
|
534
543
|
"%s: Enabled element %s, now enabled: %s",
|
535
544
|
self.file_name,
|
536
545
|
element_code.name,
|
@@ -539,55 +548,34 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
539
548
|
|
540
549
|
def disable_element(self, element_code: DrawableElement) -> None:
|
541
550
|
"""Disable drawing of a specific element."""
|
542
|
-
from .config.utils import manage_drawable_elements
|
543
551
|
manage_drawable_elements(self, "disable", element_code=element_code)
|
544
552
|
|
545
553
|
def set_elements(self, element_codes: list[DrawableElement]) -> None:
|
546
554
|
"""Enable only the specified elements, disable all others."""
|
547
|
-
from .config.utils import manage_drawable_elements
|
548
555
|
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
549
556
|
|
550
557
|
def set_element_property(
|
551
558
|
self, element_code: DrawableElement, property_name: str, value
|
552
559
|
) -> None:
|
553
560
|
"""Set a drawing property for an element."""
|
554
|
-
|
555
|
-
|
561
|
+
manage_drawable_elements(
|
562
|
+
self,
|
563
|
+
"set_property",
|
564
|
+
element_code=element_code,
|
565
|
+
property_name=property_name,
|
566
|
+
value=value,
|
567
|
+
)
|
556
568
|
|
557
569
|
def get_element_at_position(self, x: int, y: int) -> DrawableElement | None:
|
558
570
|
"""Get the element code at a specific position."""
|
559
|
-
|
560
|
-
return get_element_at_position(self.element_map, x, y)
|
571
|
+
|
572
|
+
return get_element_at_position(self.shared.element_map, x, y)
|
561
573
|
|
562
574
|
def get_room_at_position(self, x: int, y: int) -> int | None:
|
563
575
|
"""Get the room ID at a specific position, or None if not a room."""
|
564
|
-
|
565
|
-
return get_room_at_position(self.element_map, x, y, DrawableElement.ROOM_1)
|
566
|
-
|
567
|
-
@staticmethod
|
568
|
-
def blend_colors(self, base_color, overlay_color):
|
569
|
-
"""
|
570
|
-
Blend two RGBA colors, considering alpha channels.
|
571
|
-
|
572
|
-
Args:
|
573
|
-
base_color: The base RGBA color
|
574
|
-
overlay_color: The overlay RGBA color to blend on top
|
575
|
-
|
576
|
-
Returns:
|
577
|
-
The blended RGBA color
|
578
|
-
"""
|
579
|
-
from .config.utils import blend_colors
|
580
|
-
return blend_colors(base_color, overlay_color)
|
581
|
-
|
582
|
-
def blend_pixel(self, array, x, y, color, element):
|
583
|
-
"""
|
584
|
-
Blend a pixel color with the existing color at the specified position.
|
585
|
-
Also updates the element map if the new element has higher z-index.
|
586
|
-
"""
|
587
|
-
from .config.utils import blend_pixel
|
588
|
-
return blend_pixel(array, x, y, color, element, self.element_map, self.drawing_config)
|
576
|
+
return get_room_at_position(self.shared.element_map, x, y, DrawableElement.ROOM_1)
|
589
577
|
|
590
578
|
@staticmethod
|
591
|
-
async def async_copy_array(
|
579
|
+
async def async_copy_array(original_array):
|
592
580
|
"""Copy the array."""
|
593
|
-
return
|
581
|
+
return original_array.copy()
|
valetudo_map_parser/map_data.py
CHANGED
@@ -494,12 +494,3 @@ class RandImageData:
|
|
494
494
|
except KeyError:
|
495
495
|
return None
|
496
496
|
return seg_ids
|
497
|
-
|
498
|
-
# @staticmethod
|
499
|
-
# def convert_negative_angle(angle: int) -> int:
|
500
|
-
# """Convert negative angle to positive."""
|
501
|
-
# angle_c = angle % 360 # Ensure angle is within 0-359
|
502
|
-
# if angle_c < 0:
|
503
|
-
# angle_c += 360 # Convert negative angle to positive
|
504
|
-
# angle = angle_c + 180 # add offset
|
505
|
-
# return angle
|