valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b42__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 +6 -3
- valetudo_map_parser/config/auto_crop.py +1 -1
- valetudo_map_parser/config/drawable.py +155 -200
- valetudo_map_parser/config/drawable_elements.py +312 -0
- valetudo_map_parser/config/enhanced_drawable.py +447 -0
- valetudo_map_parser/config/shared.py +29 -1
- valetudo_map_parser/config/types.py +15 -7
- valetudo_map_parser/config/utils.py +410 -1
- valetudo_map_parser/hypfer_draw.py +195 -61
- valetudo_map_parser/hypfer_handler.py +344 -40
- valetudo_map_parser/rand25_handler.py +223 -38
- valetudo_map_parser-0.1.9b42.dist-info/METADATA +92 -0
- valetudo_map_parser-0.1.9b42.dist-info/RECORD +23 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/WHEEL +1 -1
- valetudo_map_parser-0.1.9b40.dist-info/METADATA +0 -23
- valetudo_map_parser-0.1.9b40.dist-info/RECORD +0 -21
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b40.dist-info → valetudo_map_parser-0.1.9b42.dist-info}/NOTICE.txt +0 -0
@@ -10,10 +10,13 @@ from __future__ import annotations
|
|
10
10
|
import json
|
11
11
|
import logging
|
12
12
|
|
13
|
+
import numpy as np
|
13
14
|
from PIL import Image
|
14
15
|
|
15
16
|
from .config.auto_crop import AutoCrop
|
16
17
|
from .config.drawable import Drawable
|
18
|
+
from .config.drawable_elements import DrawableElement, DrawingConfig
|
19
|
+
from .config.enhanced_drawable import EnhancedDrawable
|
17
20
|
from .config.shared import CameraShared
|
18
21
|
from .config.types import COLORS, CalibrationPoints, Colors, RoomsProperties, RoomStore
|
19
22
|
from .config.utils import BaseHandler, prepare_resize_params
|
@@ -35,7 +38,11 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
35
38
|
AutoCrop.__init__(self, self)
|
36
39
|
self.calibration_data = None # camera shared data.
|
37
40
|
self.data = ImageData # imported Image Data Module.
|
38
|
-
|
41
|
+
|
42
|
+
# Initialize drawing configuration using the shared utility function
|
43
|
+
from .config.utils import initialize_drawing_config
|
44
|
+
self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(self)
|
45
|
+
|
39
46
|
self.go_to = None # vacuum go to data
|
40
47
|
self.img_hash = None # hash of the image calculated to check differences.
|
41
48
|
self.img_base_layer = None # numpy array store the map base layer.
|
@@ -44,6 +51,73 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
44
51
|
self.imd = ImDraw(self) # Image Draw class.
|
45
52
|
self.color_grey = (128, 128, 128, 255)
|
46
53
|
self.file_name = self.shared.file_name # file name of the vacuum.
|
54
|
+
self.element_map = None # Map of element codes
|
55
|
+
|
56
|
+
@staticmethod
|
57
|
+
def get_corners(x_max, x_min, y_max, y_min):
|
58
|
+
"""Get the corners of the room."""
|
59
|
+
return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
|
60
|
+
|
61
|
+
async def extract_room_outline_from_map(self, room_id_int, pixels, pixel_size):
|
62
|
+
"""Extract the outline of a room using the pixel data and element map.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
room_id_int: The room ID as an integer
|
66
|
+
pixels: List of pixel coordinates in the format [[x, y, z], ...]
|
67
|
+
pixel_size: Size of each pixel
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
List of points forming the outline of the room
|
71
|
+
"""
|
72
|
+
# Calculate x and y min/max from compressed pixels for rectangular fallback
|
73
|
+
x_values = []
|
74
|
+
y_values = []
|
75
|
+
for x, y, z in pixels:
|
76
|
+
for i in range(z):
|
77
|
+
x_values.append(x + i * pixel_size)
|
78
|
+
y_values.append(y)
|
79
|
+
|
80
|
+
if not x_values or not y_values:
|
81
|
+
return []
|
82
|
+
|
83
|
+
min_x, max_x = min(x_values), max(x_values)
|
84
|
+
min_y, max_y = min(y_values), max(y_values)
|
85
|
+
|
86
|
+
# 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:
|
88
|
+
# Return rectangular outline
|
89
|
+
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
90
|
+
|
91
|
+
# Create a binary mask for this room using the pixel data
|
92
|
+
# This is more reliable than using the element_map since we're directly using the pixel data
|
93
|
+
height, width = self.element_map.shape
|
94
|
+
room_mask = np.zeros((height, width), dtype=np.uint8)
|
95
|
+
|
96
|
+
# Fill the mask with room pixels using the pixel data
|
97
|
+
for x, y, z in pixels:
|
98
|
+
for i in range(z):
|
99
|
+
px = x + i * pixel_size
|
100
|
+
py = y
|
101
|
+
# Make sure we're within bounds
|
102
|
+
if 0 <= py < height and 0 <= px < width:
|
103
|
+
# Mark a pixel_size x pixel_size block in the mask
|
104
|
+
for dx in range(pixel_size):
|
105
|
+
for dy in range(pixel_size):
|
106
|
+
if py + dy < height and px + dx < width:
|
107
|
+
room_mask[py + dy, px + dx] = 1
|
108
|
+
|
109
|
+
# Debug log to check if we have any room pixels
|
110
|
+
num_room_pixels = np.sum(room_mask)
|
111
|
+
_LOGGER.debug(
|
112
|
+
"%s: Room %s mask has %d pixels",
|
113
|
+
self.file_name, str(room_id_int), int(num_room_pixels)
|
114
|
+
)
|
115
|
+
|
116
|
+
# Use the shared utility function to extract the room outline
|
117
|
+
from .config.utils import async_extract_room_outline
|
118
|
+
return await async_extract_room_outline(
|
119
|
+
room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int, _LOGGER
|
120
|
+
)
|
47
121
|
|
48
122
|
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
49
123
|
"""Extract room properties from the JSON data."""
|
@@ -67,7 +141,27 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
67
141
|
x_max,
|
68
142
|
y_max,
|
69
143
|
) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
|
144
|
+
|
145
|
+
# Get rectangular corners as a fallback
|
70
146
|
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
|
+
|
71
165
|
room_id = str(segment_id)
|
72
166
|
self.rooms_pos.append(
|
73
167
|
{
|
@@ -77,7 +171,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
77
171
|
)
|
78
172
|
room_properties[room_id] = {
|
79
173
|
"number": segment_id,
|
80
|
-
"outline":
|
174
|
+
"outline": outline, # Use the detailed outline from the element map
|
81
175
|
"name": name,
|
82
176
|
"x": ((x_min + x_max) // 2),
|
83
177
|
"y": ((y_min + y_max) // 2),
|
@@ -138,32 +232,153 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
138
232
|
new_frame_hash = await self.calculate_array_hash(layers, active)
|
139
233
|
if self.frame_number == 0:
|
140
234
|
self.img_hash = new_frame_hash
|
141
|
-
# empty image
|
235
|
+
# Create empty image
|
142
236
|
img_np_array = await self.draw.create_empty_image(
|
143
237
|
size_x, size_y, colors["background"]
|
144
238
|
)
|
145
|
-
|
146
|
-
for
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
239
|
+
|
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
|
+
|
244
|
+
_LOGGER.info("%s: Drawing map with color blending", self.file_name)
|
245
|
+
|
246
|
+
# Draw layers and segments if enabled
|
247
|
+
room_id = 0
|
248
|
+
# Keep track of disabled rooms to skip their walls later
|
249
|
+
disabled_rooms = set()
|
250
|
+
|
251
|
+
if self.drawing_config.is_enabled(DrawableElement.FLOOR):
|
252
|
+
# First pass: identify disabled rooms
|
253
|
+
for layer_type, compressed_pixels_list in layers.items():
|
254
|
+
# Check if this is a room layer
|
255
|
+
if layer_type == "segment":
|
256
|
+
# The room_id is the current room being processed (0-based index)
|
257
|
+
# We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
|
258
|
+
current_room_id = room_id + 1
|
259
|
+
if 1 <= current_room_id <= 15:
|
260
|
+
room_element = getattr(
|
261
|
+
DrawableElement, f"ROOM_{current_room_id}", None
|
262
|
+
)
|
263
|
+
if (
|
264
|
+
room_element
|
265
|
+
and not self.drawing_config.is_enabled(
|
266
|
+
room_element
|
267
|
+
)
|
268
|
+
):
|
269
|
+
# Add this room to the disabled rooms set
|
270
|
+
disabled_rooms.add(room_id)
|
271
|
+
_LOGGER.debug(
|
272
|
+
"%s: Room %d is disabled and will be skipped",
|
273
|
+
self.file_name,
|
274
|
+
current_room_id,
|
275
|
+
)
|
276
|
+
room_id = (
|
277
|
+
room_id + 1
|
278
|
+
) % 16 # Cycle room_id back to 0 after 15
|
279
|
+
|
280
|
+
# Reset room_id for the actual drawing pass
|
281
|
+
room_id = 0
|
282
|
+
|
283
|
+
# Second pass: draw enabled rooms and walls
|
284
|
+
for layer_type, compressed_pixels_list in layers.items():
|
285
|
+
# Check if this is a room layer
|
286
|
+
is_room_layer = layer_type == "segment"
|
287
|
+
|
288
|
+
# If it's a room layer, check if the specific room is enabled
|
289
|
+
if is_room_layer:
|
290
|
+
# The room_id is the current room being processed (0-based index)
|
291
|
+
# We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
|
292
|
+
current_room_id = room_id + 1
|
293
|
+
if 1 <= current_room_id <= 15:
|
294
|
+
room_element = getattr(
|
295
|
+
DrawableElement, f"ROOM_{current_room_id}", None
|
296
|
+
)
|
297
|
+
if room_element:
|
298
|
+
# Log the room check for debugging
|
299
|
+
_LOGGER.debug(
|
300
|
+
"%s: Checking if room %d is enabled: %s",
|
301
|
+
self.file_name,
|
302
|
+
current_room_id,
|
303
|
+
self.drawing_config.is_enabled(
|
304
|
+
room_element
|
305
|
+
),
|
306
|
+
)
|
307
|
+
|
308
|
+
# Skip this room if it's disabled
|
309
|
+
if not self.drawing_config.is_enabled(
|
310
|
+
room_element
|
311
|
+
):
|
312
|
+
room_id = (
|
313
|
+
room_id + 1
|
314
|
+
) % 16 # Increment room_id even if we skip
|
315
|
+
continue
|
316
|
+
|
317
|
+
# Check if this is a wall layer and if walls are enabled
|
318
|
+
is_wall_layer = layer_type == "wall"
|
319
|
+
if is_wall_layer:
|
320
|
+
if not self.drawing_config.is_enabled(
|
321
|
+
DrawableElement.WALL
|
322
|
+
):
|
323
|
+
_LOGGER.info(
|
324
|
+
"%s: Skipping wall layer because WALL element is disabled",
|
325
|
+
self.file_name,
|
326
|
+
)
|
327
|
+
continue
|
328
|
+
|
329
|
+
# Filter out walls for disabled rooms
|
330
|
+
if disabled_rooms:
|
331
|
+
# Need to modify compressed_pixels_list to exclude walls of disabled rooms
|
332
|
+
# This requires knowledge of which walls belong to which rooms
|
333
|
+
# For now, we'll just log that we're drawing walls for all rooms
|
334
|
+
_LOGGER.debug(
|
335
|
+
"%s: Drawing walls for all rooms (including disabled ones)",
|
336
|
+
self.file_name,
|
337
|
+
)
|
338
|
+
# In a real implementation, we would filter the walls here
|
339
|
+
|
340
|
+
# Draw the layer
|
341
|
+
(
|
342
|
+
room_id,
|
343
|
+
img_np_array,
|
344
|
+
) = await self.imd.async_draw_base_layer(
|
345
|
+
img_np_array,
|
346
|
+
compressed_pixels_list,
|
347
|
+
layer_type,
|
348
|
+
colors["wall"],
|
349
|
+
colors["zone_clean"],
|
350
|
+
pixel_size,
|
351
|
+
disabled_rooms if layer_type == "wall" else None,
|
352
|
+
)
|
353
|
+
|
354
|
+
# Update element map for this layer
|
355
|
+
if is_room_layer and 0 < room_id <= 15:
|
356
|
+
# Mark the room in the element map
|
357
|
+
room_element = getattr(
|
358
|
+
DrawableElement, f"ROOM_{room_id}", None
|
359
|
+
)
|
360
|
+
if room_element:
|
361
|
+
# This is a simplification - in a real implementation we would
|
362
|
+
# need to identify the exact pixels that belong to this room
|
363
|
+
pass
|
364
|
+
|
365
|
+
# Draw the virtual walls if enabled
|
366
|
+
if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
|
367
|
+
img_np_array = await self.imd.async_draw_virtual_walls(
|
368
|
+
m_json, img_np_array, colors["no_go"]
|
369
|
+
)
|
370
|
+
|
371
|
+
# Draw charger if enabled
|
372
|
+
if self.drawing_config.is_enabled(DrawableElement.CHARGER):
|
373
|
+
img_np_array = await self.imd.async_draw_charger(
|
374
|
+
img_np_array, entity_dict, colors["charger"]
|
375
|
+
)
|
376
|
+
|
377
|
+
# Draw obstacles if enabled
|
378
|
+
if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
|
379
|
+
img_np_array = await self.imd.async_draw_obstacle(
|
380
|
+
img_np_array, entity_dict, colors["no_go"]
|
154
381
|
)
|
155
|
-
# Draw the virtual walls if any.
|
156
|
-
img_np_array = await self.imd.async_draw_virtual_walls(
|
157
|
-
m_json, img_np_array, colors["no_go"]
|
158
|
-
)
|
159
|
-
# Draw charger.
|
160
|
-
img_np_array = await self.imd.async_draw_charger(
|
161
|
-
img_np_array, entity_dict, colors["charger"]
|
162
|
-
)
|
163
|
-
# Draw obstacles if any.
|
164
|
-
img_np_array = await self.imd.async_draw_obstacle(
|
165
|
-
img_np_array, entity_dict, colors["no_go"]
|
166
|
-
)
|
167
382
|
# Robot and rooms position
|
168
383
|
if (room_id > 0) and not self.room_propriety:
|
169
384
|
self.room_propriety = await self.async_extract_room_properties(
|
@@ -193,36 +408,59 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
193
408
|
# Copy the base layer to the new image.
|
194
409
|
img_np_array = await self.async_copy_array(self.img_base_layer)
|
195
410
|
# All below will be drawn at each frame.
|
196
|
-
# Draw zones if any
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
207
|
-
|
208
|
-
|
209
|
-
|
411
|
+
# Draw zones if any and if enabled
|
412
|
+
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
413
|
+
img_np_array = await self.imd.async_draw_zones(
|
414
|
+
m_json,
|
415
|
+
img_np_array,
|
416
|
+
colors["zone_clean"],
|
417
|
+
colors["no_go"],
|
418
|
+
)
|
419
|
+
|
420
|
+
# Draw the go_to target flag if enabled
|
421
|
+
if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
|
422
|
+
img_np_array = await self.imd.draw_go_to_flag(
|
423
|
+
img_np_array, entity_dict, colors["go_to"]
|
424
|
+
)
|
425
|
+
|
426
|
+
# Draw path prediction and paths if enabled
|
427
|
+
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
428
|
+
_LOGGER.info(
|
429
|
+
"%s: PATH element enabled: %s", self.file_name, path_enabled
|
210
430
|
)
|
431
|
+
if path_enabled:
|
432
|
+
_LOGGER.info("%s: Drawing path", self.file_name)
|
433
|
+
img_np_array = await self.imd.async_draw_paths(
|
434
|
+
img_np_array, m_json, colors["move"], self.color_grey
|
435
|
+
)
|
436
|
+
else:
|
437
|
+
_LOGGER.info("%s: Skipping path drawing", self.file_name)
|
438
|
+
|
211
439
|
# Check if the robot is docked.
|
212
440
|
if self.shared.vacuum_state == "docked":
|
213
441
|
# Adjust the robot angle.
|
214
442
|
robot_position_angle -= 180
|
215
443
|
|
216
|
-
if
|
444
|
+
# Draw the robot if enabled
|
445
|
+
if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
|
446
|
+
# Get robot color (allows for customization)
|
447
|
+
robot_color = self.drawing_config.get_property(
|
448
|
+
DrawableElement.ROBOT, "color", colors["robot"]
|
449
|
+
)
|
450
|
+
|
217
451
|
# Draw the robot
|
218
452
|
img_np_array = await self.draw.robot(
|
219
453
|
layers=img_np_array,
|
220
454
|
x=robot_position[0],
|
221
455
|
y=robot_position[1],
|
222
456
|
angle=robot_position_angle,
|
223
|
-
fill=
|
457
|
+
fill=robot_color,
|
224
458
|
robot_state=self.shared.vacuum_state,
|
225
459
|
)
|
460
|
+
|
461
|
+
# Update element map for robot position
|
462
|
+
from .config.utils import update_element_map_with_robot
|
463
|
+
update_element_map_with_robot(self.element_map, robot_position, DrawableElement.ROBOT)
|
226
464
|
# Resize the image
|
227
465
|
img_np_array = await self.async_auto_trim_and_zoom_image(
|
228
466
|
img_np_array,
|
@@ -287,3 +525,69 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
287
525
|
calibration_data.append(calibration_point)
|
288
526
|
del vacuum_points, map_points, calibration_point, rotation_angle # free memory.
|
289
527
|
return calibration_data
|
528
|
+
|
529
|
+
# Element selection methods
|
530
|
+
def enable_element(self, element_code: DrawableElement) -> None:
|
531
|
+
"""Enable drawing of a specific element."""
|
532
|
+
self.drawing_config.enable_element(element_code)
|
533
|
+
_LOGGER.info(
|
534
|
+
"%s: Enabled element %s, now enabled: %s",
|
535
|
+
self.file_name,
|
536
|
+
element_code.name,
|
537
|
+
self.drawing_config.is_enabled(element_code),
|
538
|
+
)
|
539
|
+
|
540
|
+
def disable_element(self, element_code: DrawableElement) -> None:
|
541
|
+
"""Disable drawing of a specific element."""
|
542
|
+
from .config.utils import manage_drawable_elements
|
543
|
+
manage_drawable_elements(self, "disable", element_code=element_code)
|
544
|
+
|
545
|
+
def set_elements(self, element_codes: list[DrawableElement]) -> None:
|
546
|
+
"""Enable only the specified elements, disable all others."""
|
547
|
+
from .config.utils import manage_drawable_elements
|
548
|
+
manage_drawable_elements(self, "set_elements", element_codes=element_codes)
|
549
|
+
|
550
|
+
def set_element_property(
|
551
|
+
self, element_code: DrawableElement, property_name: str, value
|
552
|
+
) -> None:
|
553
|
+
"""Set a drawing property for an element."""
|
554
|
+
from .config.utils import manage_drawable_elements
|
555
|
+
manage_drawable_elements(self, "set_property", element_code=element_code, property_name=property_name, value=value)
|
556
|
+
|
557
|
+
def get_element_at_position(self, x: int, y: int) -> DrawableElement | None:
|
558
|
+
"""Get the element code at a specific position."""
|
559
|
+
from .config.utils import get_element_at_position
|
560
|
+
return get_element_at_position(self.element_map, x, y)
|
561
|
+
|
562
|
+
def get_room_at_position(self, x: int, y: int) -> int | None:
|
563
|
+
"""Get the room ID at a specific position, or None if not a room."""
|
564
|
+
from .config.utils import get_room_at_position
|
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)
|
589
|
+
|
590
|
+
@staticmethod
|
591
|
+
async def async_copy_array(array):
|
592
|
+
"""Copy the array."""
|
593
|
+
return array.copy()
|