valetudo-map-parser 0.1.9b43__py3-none-any.whl → 0.1.9b44__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 +60 -8
- valetudo_map_parser/config/drawable.py +4 -0
- 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 +96 -63
- valetudo_map_parser/map_data.py +0 -9
- valetudo_map_parser/rand25_handler.py +170 -129
- 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.9b44.dist-info}/METADATA +3 -1
- valetudo_map_parser-0.1.9b44.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.9b44.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b44.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b43.dist-info → valetudo_map_parser-0.1.9b44.dist-info}/WHEEL +0 -0
@@ -8,25 +8,39 @@ 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
|
+
blend_colors,
|
30
|
+
blend_pixel,
|
31
|
+
get_element_at_position,
|
32
|
+
get_room_at_position,
|
33
|
+
handle_room_outline_error,
|
34
|
+
initialize_drawing_config,
|
35
|
+
manage_drawable_elements,
|
36
|
+
prepare_resize_params,
|
37
|
+
update_element_map_with_robot,
|
38
|
+
)
|
39
|
+
from .config.room_outline import extract_room_outline_with_scipy
|
23
40
|
from .hypfer_draw import ImageDraw as ImDraw
|
24
41
|
from .map_data import ImageData
|
25
42
|
|
26
43
|
|
27
|
-
_LOGGER = logging.getLogger(__name__)
|
28
|
-
|
29
|
-
|
30
44
|
class HypferMapImageHandler(BaseHandler, AutoCrop):
|
31
45
|
"""Map Image Handler Class.
|
32
46
|
This class is used to handle the image data and the drawing of the map."""
|
@@ -40,8 +54,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
40
54
|
self.data = ImageData # imported Image Data Module.
|
41
55
|
|
42
56
|
# Initialize drawing configuration using the shared utility function
|
43
|
-
|
44
|
-
|
57
|
+
self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
|
58
|
+
self
|
59
|
+
)
|
45
60
|
|
46
61
|
self.go_to = None # vacuum go to data
|
47
62
|
self.img_hash = None # hash of the image calculated to check differences.
|
@@ -51,13 +66,15 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
51
66
|
self.imd = ImDraw(self) # Image Draw class.
|
52
67
|
self.color_grey = (128, 128, 128, 255)
|
53
68
|
self.file_name = self.shared.file_name # file name of the vacuum.
|
54
|
-
self.
|
69
|
+
self.element_map_manager = OptimizedElementMapGenerator(self.drawing_config,
|
70
|
+
self.shared) # Map of element codes
|
55
71
|
|
56
72
|
@staticmethod
|
57
73
|
def get_corners(x_max, x_min, y_max, y_min):
|
58
74
|
"""Get the corners of the room."""
|
59
75
|
return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
60
76
|
|
77
|
+
|
61
78
|
async def extract_room_outline_from_map(self, room_id_int, pixels, pixel_size):
|
62
79
|
"""Extract the outline of a room using the pixel data and element map.
|
63
80
|
|
@@ -84,13 +101,13 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
84
101
|
min_y, max_y = min(y_values), max(y_values)
|
85
102
|
|
86
103
|
# 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:
|
104
|
+
if not hasattr(self, "element_map") or self.shared.element_map is None:
|
88
105
|
# Return rectangular outline
|
89
106
|
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
90
107
|
|
91
108
|
# Create a binary mask for this room using the pixel data
|
92
109
|
# This is more reliable than using the element_map since we're directly using the pixel data
|
93
|
-
height, width = self.element_map.shape
|
110
|
+
height, width = self.shared.element_map.shape
|
94
111
|
room_mask = np.zeros((height, width), dtype=np.uint8)
|
95
112
|
|
96
113
|
# Fill the mask with room pixels using the pixel data
|
@@ -108,15 +125,16 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
108
125
|
|
109
126
|
# Debug log to check if we have any room pixels
|
110
127
|
num_room_pixels = np.sum(room_mask)
|
111
|
-
|
128
|
+
LOGGER.debug(
|
112
129
|
"%s: Room %s mask has %d pixels",
|
113
|
-
self.file_name,
|
130
|
+
self.file_name,
|
131
|
+
str(room_id_int),
|
132
|
+
int(num_room_pixels),
|
114
133
|
)
|
115
134
|
|
116
|
-
# Use the
|
117
|
-
|
118
|
-
|
119
|
-
room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int, _LOGGER
|
135
|
+
# Use the scipy-based room outline extraction
|
136
|
+
return await extract_room_outline_with_scipy(
|
137
|
+
room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int
|
120
138
|
)
|
121
139
|
|
122
140
|
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
@@ -151,15 +169,14 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
151
169
|
outline = await self.extract_room_outline_from_map(
|
152
170
|
segment_id, pixels, pixel_size
|
153
171
|
)
|
154
|
-
|
172
|
+
LOGGER.debug(
|
155
173
|
"%s: Traced outline for room %s with %d points",
|
156
174
|
self.file_name,
|
157
175
|
segment_id,
|
158
176
|
len(outline),
|
159
177
|
)
|
160
178
|
except (ValueError, IndexError, TypeError, ArithmeticError) as e:
|
161
|
-
|
162
|
-
handle_room_outline_error(self.file_name, segment_id, e, _LOGGER)
|
179
|
+
handle_room_outline_error(self.file_name, segment_id, e)
|
163
180
|
outline = corners
|
164
181
|
|
165
182
|
room_id = str(segment_id)
|
@@ -178,11 +195,11 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
178
195
|
}
|
179
196
|
if room_properties:
|
180
197
|
rooms = RoomStore(self.file_name, room_properties)
|
181
|
-
|
198
|
+
LOGGER.debug(
|
182
199
|
"%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
|
183
200
|
)
|
184
201
|
else:
|
185
|
-
|
202
|
+
LOGGER.debug("%s: Rooms data not available!", self.file_name)
|
186
203
|
self.rooms_pos = None
|
187
204
|
return room_properties
|
188
205
|
|
@@ -204,7 +221,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
204
221
|
# Check if the JSON data is not None else process the image.
|
205
222
|
try:
|
206
223
|
if m_json is not None:
|
207
|
-
|
224
|
+
LOGGER.debug("%s: Creating Image.", self.file_name)
|
208
225
|
# buffer json data
|
209
226
|
self.json_data = m_json
|
210
227
|
# Get the image size from the JSON data
|
@@ -237,11 +254,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
237
254
|
size_x, size_y, colors["background"]
|
238
255
|
)
|
239
256
|
|
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
257
|
|
244
|
-
|
258
|
+
LOGGER.info("%s: Drawing map with color blending", self.file_name)
|
245
259
|
|
246
260
|
# Draw layers and segments if enabled
|
247
261
|
room_id = 0
|
@@ -268,7 +282,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
268
282
|
):
|
269
283
|
# Add this room to the disabled rooms set
|
270
284
|
disabled_rooms.add(room_id)
|
271
|
-
|
285
|
+
LOGGER.debug(
|
272
286
|
"%s: Room %d is disabled and will be skipped",
|
273
287
|
self.file_name,
|
274
288
|
current_room_id,
|
@@ -296,7 +310,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
296
310
|
)
|
297
311
|
if room_element:
|
298
312
|
# Log the room check for debugging
|
299
|
-
|
313
|
+
LOGGER.debug(
|
300
314
|
"%s: Checking if room %d is enabled: %s",
|
301
315
|
self.file_name,
|
302
316
|
current_room_id,
|
@@ -320,7 +334,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
320
334
|
if not self.drawing_config.is_enabled(
|
321
335
|
DrawableElement.WALL
|
322
336
|
):
|
323
|
-
|
337
|
+
LOGGER.info(
|
324
338
|
"%s: Skipping wall layer because WALL element is disabled",
|
325
339
|
self.file_name,
|
326
340
|
)
|
@@ -331,7 +345,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
331
345
|
# Need to modify compressed_pixels_list to exclude walls of disabled rooms
|
332
346
|
# This requires knowledge of which walls belong to which rooms
|
333
347
|
# For now, we'll just log that we're drawing walls for all rooms
|
334
|
-
|
348
|
+
LOGGER.debug(
|
335
349
|
"%s: Drawing walls for all rooms (including disabled ones)",
|
336
350
|
self.file_name,
|
337
351
|
)
|
@@ -390,7 +404,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
390
404
|
robot_y=(robot_position[1]),
|
391
405
|
angle=robot_position_angle,
|
392
406
|
)
|
393
|
-
|
407
|
+
LOGGER.info("%s: Completed base Layers", self.file_name)
|
394
408
|
# Copy the new array in base layer.
|
395
409
|
self.img_base_layer = await self.async_copy_array(img_np_array)
|
396
410
|
self.shared.frame_number = self.frame_number
|
@@ -399,7 +413,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
399
413
|
new_frame_hash != self.img_hash
|
400
414
|
):
|
401
415
|
self.frame_number = 0
|
402
|
-
|
416
|
+
LOGGER.debug(
|
403
417
|
"%s: %s at Frame Number: %s",
|
404
418
|
self.file_name,
|
405
419
|
str(self.json_id),
|
@@ -425,16 +439,16 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
425
439
|
|
426
440
|
# Draw path prediction and paths if enabled
|
427
441
|
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
428
|
-
|
442
|
+
LOGGER.info(
|
429
443
|
"%s: PATH element enabled: %s", self.file_name, path_enabled
|
430
444
|
)
|
431
445
|
if path_enabled:
|
432
|
-
|
446
|
+
LOGGER.info("%s: Drawing path", self.file_name)
|
433
447
|
img_np_array = await self.imd.async_draw_paths(
|
434
448
|
img_np_array, m_json, colors["move"], self.color_grey
|
435
449
|
)
|
436
450
|
else:
|
437
|
-
|
451
|
+
LOGGER.info("%s: Skipping path drawing", self.file_name)
|
438
452
|
|
439
453
|
# Check if the robot is docked.
|
440
454
|
if self.shared.vacuum_state == "docked":
|
@@ -459,8 +473,10 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
459
473
|
)
|
460
474
|
|
461
475
|
# Update element map for robot position
|
462
|
-
|
463
|
-
|
476
|
+
if hasattr(self.shared, 'element_map') and self.shared.element_map is not None:
|
477
|
+
update_element_map_with_robot(
|
478
|
+
self.shared.element_map, robot_position, DrawableElement.ROBOT
|
479
|
+
)
|
464
480
|
# Resize the image
|
465
481
|
img_np_array = await self.async_auto_trim_and_zoom_image(
|
466
482
|
img_np_array,
|
@@ -471,9 +487,24 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
471
487
|
)
|
472
488
|
# If the image is None return None and log the error.
|
473
489
|
if img_np_array is None:
|
474
|
-
|
490
|
+
LOGGER.warning("%s: Image array is None.", self.file_name)
|
475
491
|
return None
|
476
|
-
|
492
|
+
# Debug logging for element map creation
|
493
|
+
LOGGER.info("%s: Frame number: %d, has element_map: %s",
|
494
|
+
self.file_name, self.frame_number, hasattr(self.shared, 'element_map'))
|
495
|
+
|
496
|
+
if (self.shared.element_map is None) and (self.frame_number == 1):
|
497
|
+
# Create element map for tracking what's drawn where
|
498
|
+
LOGGER.info("%s: Creating element map with shape: %s",
|
499
|
+
self.file_name, img_np_array.shape)
|
500
|
+
|
501
|
+
# Generate the element map directly from JSON data
|
502
|
+
# This will create a cropped element map containing only the non-zero elements
|
503
|
+
LOGGER.info("%s: Generating element map from JSON data", self.file_name)
|
504
|
+
self.shared.element_map = await self.element_map_manager.async_generate_from_json(m_json)
|
505
|
+
|
506
|
+
LOGGER.info("%s: Element map created with shape: %s",
|
507
|
+
self.file_name, self.shared.element_map.shape if self.shared.element_map is not None else None)
|
477
508
|
# Convert the numpy array to a PIL image
|
478
509
|
pil_img = Image.fromarray(img_np_array, mode="RGBA")
|
479
510
|
del img_np_array
|
@@ -482,10 +513,10 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
482
513
|
resize_params = prepare_resize_params(self, pil_img, False)
|
483
514
|
resized_image = await self.async_resize_images(resize_params)
|
484
515
|
return resized_image
|
485
|
-
|
516
|
+
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
486
517
|
return pil_img
|
487
518
|
except (RuntimeError, RuntimeWarning) as e:
|
488
|
-
|
519
|
+
LOGGER.warning(
|
489
520
|
"%s: Error %s during image creation.",
|
490
521
|
self.file_name,
|
491
522
|
str(e),
|
@@ -499,12 +530,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
499
530
|
if self.room_propriety:
|
500
531
|
return self.room_propriety
|
501
532
|
if self.json_data:
|
502
|
-
|
533
|
+
LOGGER.debug("Checking %s Rooms data..", self.file_name)
|
503
534
|
self.room_propriety = await self.async_extract_room_properties(
|
504
535
|
self.json_data
|
505
536
|
)
|
506
537
|
if self.room_propriety:
|
507
|
-
|
538
|
+
LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
|
508
539
|
return self.room_propriety
|
509
540
|
|
510
541
|
def get_calibration_data(self) -> CalibrationPoints:
|
@@ -512,7 +543,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
512
543
|
this will create the attribute calibration points."""
|
513
544
|
calibration_data = []
|
514
545
|
rotation_angle = self.shared.image_rotate
|
515
|
-
|
546
|
+
LOGGER.info("Getting %s Calibrations points.", self.file_name)
|
516
547
|
|
517
548
|
# Define the map points (fixed)
|
518
549
|
map_points = self.get_map_points()
|
@@ -530,7 +561,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
530
561
|
def enable_element(self, element_code: DrawableElement) -> None:
|
531
562
|
"""Enable drawing of a specific element."""
|
532
563
|
self.drawing_config.enable_element(element_code)
|
533
|
-
|
564
|
+
LOGGER.info(
|
534
565
|
"%s: Enabled element %s, now enabled: %s",
|
535
566
|
self.file_name,
|
536
567
|
element_code.name,
|
@@ -539,33 +570,35 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
539
570
|
|
540
571
|
def disable_element(self, element_code: DrawableElement) -> None:
|
541
572
|
"""Disable drawing of a specific element."""
|
542
|
-
from .config.utils import manage_drawable_elements
|
543
573
|
manage_drawable_elements(self, "disable", element_code=element_code)
|
544
574
|
|
545
575
|
def set_elements(self, element_codes: list[DrawableElement]) -> None:
|
546
576
|
"""Enable only the specified elements, disable all others."""
|
547
|
-
from .config.utils import manage_drawable_elements
|
548
577
|
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
549
578
|
|
550
579
|
def set_element_property(
|
551
580
|
self, element_code: DrawableElement, property_name: str, value
|
552
581
|
) -> None:
|
553
582
|
"""Set a drawing property for an element."""
|
554
|
-
|
555
|
-
|
583
|
+
manage_drawable_elements(
|
584
|
+
self,
|
585
|
+
"set_property",
|
586
|
+
element_code=element_code,
|
587
|
+
property_name=property_name,
|
588
|
+
value=value,
|
589
|
+
)
|
556
590
|
|
557
591
|
def get_element_at_position(self, x: int, y: int) -> DrawableElement | None:
|
558
592
|
"""Get the element code at a specific position."""
|
559
|
-
|
560
|
-
return get_element_at_position(self.element_map, x, y)
|
593
|
+
|
594
|
+
return get_element_at_position(self.shared.element_map, x, y)
|
561
595
|
|
562
596
|
def get_room_at_position(self, x: int, y: int) -> int | None:
|
563
597
|
"""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)
|
598
|
+
return get_room_at_position(self.shared.element_map, x, y, DrawableElement.ROOM_1)
|
566
599
|
|
567
600
|
@staticmethod
|
568
|
-
def blend_colors(
|
601
|
+
def blend_colors(base_color, overlay_color):
|
569
602
|
"""
|
570
603
|
Blend two RGBA colors, considering alpha channels.
|
571
604
|
|
@@ -576,7 +609,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
576
609
|
Returns:
|
577
610
|
The blended RGBA color
|
578
611
|
"""
|
579
|
-
from .config.utils import blend_colors
|
580
612
|
return blend_colors(base_color, overlay_color)
|
581
613
|
|
582
614
|
def blend_pixel(self, array, x, y, color, element):
|
@@ -584,10 +616,11 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
584
616
|
Blend a pixel color with the existing color at the specified position.
|
585
617
|
Also updates the element map if the new element has higher z-index.
|
586
618
|
"""
|
587
|
-
|
588
|
-
|
619
|
+
return blend_pixel(
|
620
|
+
array, x, y, color, element, self.shared.element_map, self.drawing_config
|
621
|
+
)
|
589
622
|
|
590
623
|
@staticmethod
|
591
|
-
async def async_copy_array(
|
624
|
+
async def async_copy_array(original_array):
|
592
625
|
"""Copy the array."""
|
593
|
-
return
|
626
|
+
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
|