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.
@@ -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.drawable import Drawable
18
- from .config.drawable_elements import DrawableElement, DrawingConfig
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 COLORS, CalibrationPoints, Colors, RoomsProperties, RoomStore
22
- from .config.utils import BaseHandler, prepare_resize_params
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
- from .config.utils import initialize_drawing_config
44
- self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(self)
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.element_map = None # Map of element codes
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
- _LOGGER.debug(
125
+ LOGGER.debug(
112
126
  "%s: Room %s mask has %d pixels",
113
- self.file_name, str(room_id_int), int(num_room_pixels)
127
+ self.file_name,
128
+ str(room_id_int),
129
+ int(num_room_pixels),
114
130
  )
115
131
 
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
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": outline, # Use the detailed outline from the element map
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
- _LOGGER.debug(
176
+ LOGGER.debug(
182
177
  "%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
183
178
  )
184
179
  else:
185
- _LOGGER.debug("%s: Rooms data not available!", self.file_name)
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
- _LOGGER.debug("%s: Creating Image.", self.file_name)
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
- _LOGGER.info("%s: Drawing map with color blending", self.file_name)
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
- _LOGGER.debug(
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
- _LOGGER.debug(
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
- _LOGGER.info(
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
- _LOGGER.debug(
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
- _LOGGER.info("%s: Completed base Layers", self.file_name)
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
- _LOGGER.debug(
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
- _LOGGER.info(
420
+ LOGGER.info(
429
421
  "%s: PATH element enabled: %s", self.file_name, path_enabled
430
422
  )
431
423
  if path_enabled:
432
- _LOGGER.info("%s: Drawing path", self.file_name)
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
- _LOGGER.info("%s: Skipping path drawing", self.file_name)
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
- from .config.utils import update_element_map_with_robot
463
- update_element_map_with_robot(self.element_map, robot_position, DrawableElement.ROBOT)
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
- _LOGGER.warning("%s: Image array is None.", self.file_name)
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
- _LOGGER.debug("%s: Frame Completed.", self.file_name)
494
+ LOGGER.debug("%s: Frame Completed.", self.file_name)
486
495
  return pil_img
487
496
  except (RuntimeError, RuntimeWarning) as e:
488
- _LOGGER.warning(
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
- _LOGGER.debug("Checking %s Rooms data..", self.file_name)
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
- _LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
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
- _LOGGER.info("Getting %s Calibrations points.", self.file_name)
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
- _LOGGER.info(
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
- from .config.utils import manage_drawable_elements
555
- manage_drawable_elements(self, "set_property", element_code=element_code, property_name=property_name, value=value)
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
- from .config.utils import get_element_at_position
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
- 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)
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(array):
579
+ async def async_copy_array(original_array):
592
580
  """Copy the array."""
593
- return array.copy()
581
+ return original_array.copy()
@@ -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