valetudo-map-parser 0.1.10rc6__py3-none-any.whl → 0.1.11b0__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.
@@ -1,32 +1,30 @@
1
1
  """Utility code for the valetudo map parser."""
2
2
 
3
3
  import datetime
4
- from time import time
5
4
  import hashlib
5
+ import io
6
6
  import json
7
7
  from dataclasses import dataclass
8
+ from time import time
8
9
  from typing import Callable, List, Optional, Tuple
9
- import io
10
10
 
11
11
  import numpy as np
12
12
  from PIL import Image, ImageOps
13
13
 
14
+ from ..map_data import HyperMapData
15
+ from .async_utils import AsyncNumPy
14
16
  from .drawable import Drawable
15
17
  from .drawable_elements import DrawingConfig
16
- from .enhanced_drawable import EnhancedDrawable
17
18
  from .status_text.status_text import StatusText
18
-
19
19
  from .types import (
20
20
  LOGGER,
21
21
  ChargerPosition,
22
- Size,
22
+ Destinations,
23
23
  NumpyArray,
24
24
  PilPNG,
25
25
  RobotPosition,
26
- Destinations
26
+ Size,
27
27
  )
28
- from ..map_data import HyperMapData
29
- from .async_utils import AsyncNumPy
30
28
 
31
29
 
32
30
  @dataclass
@@ -79,7 +77,6 @@ class BaseHandler:
79
77
  # Drawing components are initialized by initialize_drawing_config in handlers
80
78
  self.drawing_config: Optional[DrawingConfig] = None
81
79
  self.draw: Optional[Drawable] = None
82
- self.enhanced_draw: Optional[EnhancedDrawable] = None
83
80
 
84
81
  def get_frame_number(self) -> int:
85
82
  """Return the frame number of the image."""
@@ -114,6 +111,12 @@ class BaseHandler:
114
111
  try:
115
112
  # Backup current image to last_image before processing new one
116
113
  if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
114
+ # Close old last_image to free memory before replacing it
115
+ if hasattr(self.shared, "last_image") and self.shared.last_image is not None:
116
+ try:
117
+ self.shared.last_image.close()
118
+ except Exception:
119
+ pass # Ignore errors if image is already closed
117
120
  self.shared.last_image = self.shared.new_image
118
121
 
119
122
  # Call the appropriate handler method based on handler type
@@ -174,13 +177,11 @@ class BaseHandler:
174
177
  LOGGER.warning(
175
178
  "%s: Failed to generate image from JSON data", self.file_name
176
179
  )
177
- if bytes_format and hasattr(self.shared, "last_image"):
178
- return pil_to_png_bytes(self.shared.last_image), {}
179
180
  return (
180
181
  self.shared.last_image
181
182
  if hasattr(self.shared, "last_image")
182
183
  else None
183
- ), {}
184
+ ), self.shared.to_dict()
184
185
 
185
186
  except Exception as e:
186
187
  LOGGER.warning(
@@ -197,26 +198,28 @@ class BaseHandler:
197
198
  """Update the shared data with the latest information."""
198
199
 
199
200
  if hasattr(self, "get_rooms_attributes") and (
200
- self.shared.map_rooms is None and destinations is not None
201
+ self.shared.map_rooms is None and destinations is not None
201
202
  ):
202
- (
203
- self.shared.map_rooms,
204
- self.shared.map_pred_zones,
205
- self.shared.map_pred_points,
206
- ) = await self.get_rooms_attributes(destinations)
203
+ self.shared.map_rooms = await self.get_rooms_attributes(destinations)
207
204
  if self.shared.map_rooms:
208
205
  LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
209
206
 
207
+
210
208
  if hasattr(self, "async_get_rooms_attributes") and (
211
- self.shared.map_rooms is None
209
+ self.shared.map_rooms is None
212
210
  ):
213
211
  if self.shared.map_rooms is None:
214
212
  self.shared.map_rooms = await self.async_get_rooms_attributes()
215
213
  if self.shared.map_rooms:
216
214
  LOGGER.debug("%s: Hyper attributes rooms updated", self.file_name)
217
215
 
218
- if hasattr(self, "get_calibration_data") and self.shared.attr_calibration_points is None:
219
- self.shared.attr_calibration_points = self.get_calibration_data(self.shared.image_rotate)
216
+ if (
217
+ hasattr(self, "get_calibration_data")
218
+ and self.shared.attr_calibration_points is None
219
+ ):
220
+ self.shared.attr_calibration_points = self.get_calibration_data(
221
+ self.shared.image_rotate
222
+ )
220
223
 
221
224
  if not self.shared.image_size:
222
225
  self.shared.image_size = self.get_img_size()
@@ -228,14 +231,12 @@ class BaseHandler:
228
231
 
229
232
  self.shared.current_room = self.get_robot_position()
230
233
 
231
- def prepare_resize_params(self, pil_img: PilPNG, rand: bool=False) -> ResizeParams:
234
+ def prepare_resize_params(
235
+ self, pil_img: PilPNG, rand: bool = False
236
+ ) -> ResizeParams:
232
237
  """Prepare resize parameters for image resizing."""
233
- if self.shared.image_rotate in [0, 180]:
234
- width, height = pil_img.size
235
- else:
236
- height, width = pil_img.size
237
- LOGGER.debug("Shared PIL image size: %s x %s", self.shared.image_ref_width,
238
- self.shared.image_ref_height)
238
+ width, height = pil_size_rotation(self.shared.image_rotate, pil_img)
239
+
239
240
  return ResizeParams(
240
241
  pil_img=pil_img,
241
242
  width=width,
@@ -656,9 +657,6 @@ class BaseHandler:
656
657
 
657
658
  async def async_resize_image(params: ResizeParams):
658
659
  """Resize the image to the given dimensions and aspect ratio."""
659
- LOGGER.debug("Resizing image to aspect ratio: %s", params.aspect_ratio)
660
- LOGGER.debug("Original image size: %s x %s", params.width, params.height)
661
- LOGGER.debug("Image crop size: %s", params.crop_size)
662
660
  if params.aspect_ratio == "None":
663
661
  return params.pil_img
664
662
  if params.aspect_ratio != "None":
@@ -695,6 +693,17 @@ async def async_resize_image(params: ResizeParams):
695
693
  return params.pil_img
696
694
 
697
695
 
696
+ def pil_size_rotation(image_rotate, pil_img):
697
+ """Return the size of the image."""
698
+ if not pil_img:
699
+ return 0, 0
700
+ if image_rotate in [0, 180]:
701
+ width, height = pil_img.size
702
+ else:
703
+ height, width = pil_img.size
704
+ return width, height
705
+
706
+
698
707
  def initialize_drawing_config(handler):
699
708
  """
700
709
  Initialize drawing configuration from device_info.
@@ -703,7 +712,7 @@ def initialize_drawing_config(handler):
703
712
  handler: The handler instance with shared data and file_name attributes
704
713
 
705
714
  Returns:
706
- Tuple of (DrawingConfig, Drawable, EnhancedDrawable)
715
+ Tuple of (DrawingConfig, Drawable)
707
716
  """
708
717
 
709
718
  # Initialize drawing configuration
@@ -715,98 +724,10 @@ def initialize_drawing_config(handler):
715
724
  ):
716
725
  drawing_config.update_from_device_info(handler.shared.device_info)
717
726
 
718
- # Initialize both drawable systems for backward compatibility
719
- draw = Drawable() # Legacy drawing utilities
720
- enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
721
-
722
- return drawing_config, draw, enhanced_draw
727
+ # Initialize drawing utilities
728
+ draw = Drawable()
723
729
 
724
-
725
- def blend_colors(base_color, overlay_color):
726
- """
727
- Blend two RGBA colors using alpha compositing.
728
-
729
- Args:
730
- base_color: Base RGBA color tuple (r, g, b, a)
731
- overlay_color: Overlay RGBA color tuple (r, g, b, a)
732
-
733
- Returns:
734
- Blended RGBA color tuple (r, g, b, a)
735
- """
736
- r1, g1, b1, a1 = base_color
737
- r2, g2, b2, a2 = overlay_color
738
-
739
- # Convert alpha to 0-1 range
740
- a1 = a1 / 255.0
741
- a2 = a2 / 255.0
742
-
743
- # Calculate resulting alpha
744
- a_out = a1 + a2 * (1 - a1)
745
-
746
- # Avoid division by zero
747
- if a_out < 0.0001:
748
- return [0, 0, 0, 0]
749
-
750
- # Calculate blended RGB components
751
- r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
752
- g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
753
- b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
754
-
755
- # Convert back to 0-255 range and return as tuple
756
- return (
757
- int(max(0, min(255, r_out))),
758
- int(max(0, min(255, g_out))),
759
- int(max(0, min(255, b_out))),
760
- int(max(0, min(255, a_out * 255))),
761
- )
762
-
763
-
764
- def blend_pixel(array, x, y, color, element, element_map=None, drawing_config=None):
765
- """
766
- Blend a pixel color with the existing color at the specified position.
767
- Also updates the element map if the new element has higher z-index.
768
-
769
- Args:
770
- array: The image array to modify
771
- x: X coordinate
772
- y: Y coordinate
773
- color: RGBA color tuple to blend
774
- element: Element code for the pixel
775
- element_map: Optional element map to update
776
- drawing_config: Optional drawing configuration for z-index lookup
777
-
778
- Returns:
779
- None
780
- """
781
- # Check bounds
782
- if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
783
- return
784
-
785
- # Get current element at this position
786
- current_element = None
787
- if element_map is not None:
788
- current_element = element_map[y, x]
789
-
790
- # Get z-index values for comparison
791
- current_z = 0
792
- new_z = 0
793
-
794
- if drawing_config is not None:
795
- current_z = (
796
- drawing_config.get_property(current_element, "z_index", 0)
797
- if current_element
798
- else 0
799
- )
800
- new_z = drawing_config.get_property(element, "z_index", 0)
801
-
802
- # Update element map if new element has higher z-index
803
- if element_map is not None and new_z >= current_z:
804
- element_map[y, x] = element
805
-
806
- # Blend colors
807
- base_color = array[y, x]
808
- blended_color = blend_colors(base_color, color)
809
- array[y, x] = blended_color
730
+ return drawing_config, draw
810
731
 
811
732
 
812
733
  def manage_drawable_elements(
@@ -990,12 +911,6 @@ async def async_extract_room_outline(
990
911
 
991
912
  # If we found too few boundary points, use the rectangular outline
992
913
  if len(boundary_points) < 8: # Need at least 8 points for a meaningful shape
993
- LOGGER.debug(
994
- "%s: Room %s has too few boundary points (%d), using rectangular outline",
995
- file_name,
996
- str(room_id_int),
997
- len(boundary_points),
998
- )
999
914
  return rect_outline
1000
915
 
1001
916
  # Use a more sophisticated algorithm to create a coherent outline
@@ -1031,13 +946,6 @@ async def async_extract_room_outline(
1031
946
  # Convert NumPy int64 values to regular Python integers
1032
947
  simplified_outline = [(int(x), int(y)) for x, y in simplified_outline]
1033
948
 
1034
- LOGGER.debug(
1035
- "%s: Room %s outline has %d points",
1036
- file_name,
1037
- str(room_id_int),
1038
- len(simplified_outline),
1039
- )
1040
-
1041
949
  return simplified_outline
1042
950
 
1043
951
  except (ValueError, IndexError, TypeError, ArithmeticError) as e:
@@ -269,8 +269,6 @@ class ImageDraw:
269
269
  zone_clean = self.img_h.data.find_zone_entities(m_json)
270
270
  except (ValueError, KeyError):
271
271
  zone_clean = None
272
- else:
273
- _LOGGER.info("%s: Got zones.", self.file_name)
274
272
 
275
273
  if zone_clean:
276
274
  # Process zones sequentially to avoid memory-intensive array copies
@@ -8,25 +8,22 @@ Version: 0.1.10
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
- import numpy as np
12
11
 
12
+ import numpy as np
13
+ from mvcrender.autocrop import AutoCrop
13
14
  from PIL import Image
14
15
 
15
16
  from .config.async_utils import AsyncPIL
16
-
17
- # from .config.auto_crop import AutoCrop
18
- from mvcrender.autocrop import AutoCrop
19
17
  from .config.drawable_elements import DrawableElement
20
18
  from .config.shared import CameraShared
21
-
22
19
  from .config.types import (
23
20
  COLORS,
24
21
  LOGGER,
25
22
  CalibrationPoints,
26
23
  Colors,
24
+ JsonType,
27
25
  RoomsProperties,
28
26
  RoomStore,
29
- JsonType,
30
27
  )
31
28
  from .config.utils import (
32
29
  BaseHandler,
@@ -49,9 +46,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
49
46
  self.calibration_data = None # camera shared data.
50
47
  self.data = ImageData # imported Image Data Module.
51
48
  # Initialize drawing configuration using the shared utility function
52
- self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
53
- self
54
- )
49
+ self.drawing_config, self.draw = initialize_drawing_config(self)
55
50
 
56
51
  self.go_to = None # vacuum go to data
57
52
  self.img_hash = None # hash of the image calculated to check differences.
@@ -60,7 +55,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
60
55
  None # persistent working buffer to avoid per-frame allocations
61
56
  )
62
57
  self.active_zones = [] # vacuum active zones.
63
- self.svg_wait = False # SVG image creation wait.
64
58
  self.imd = ImDraw(self) # Image Draw class.
65
59
  self.color_grey = (128, 128, 128, 255)
66
60
  self.file_name = self.shared.file_name # file name of the vacuum.
@@ -79,7 +73,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
79
73
  json_data
80
74
  )
81
75
  if room_properties:
82
- rooms = RoomStore(self.file_name, room_properties)
76
+ _ = RoomStore(self.file_name, room_properties)
83
77
  # Convert room_properties to the format expected by async_get_robot_in_room
84
78
  self.rooms_pos = []
85
79
  for room_id, room_data in room_properties.items():
@@ -260,7 +254,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
260
254
  )
261
255
  LOGGER.info("%s: Completed base Layers", self.file_name)
262
256
  # Copy the new array in base layer.
257
+ # Delete old base layer before creating new one to free memory
258
+ if self.img_base_layer is not None:
259
+ del self.img_base_layer
263
260
  self.img_base_layer = await self.async_copy_array(img_np_array)
261
+ # Delete source array after copying to free memory
262
+ del img_np_array
264
263
 
265
264
  self.shared.frame_number = self.frame_number
266
265
  self.frame_number += 1
@@ -274,6 +273,9 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
274
273
  or self.img_work_layer.shape != self.img_base_layer.shape
275
274
  or self.img_work_layer.dtype != self.img_base_layer.dtype
276
275
  ):
276
+ # Delete old buffer before creating new one to free memory
277
+ if self.img_work_layer is not None:
278
+ del self.img_work_layer
277
279
  self.img_work_layer = np.empty_like(self.img_base_layer)
278
280
 
279
281
  # Copy the base layer into the persistent working buffer (no new allocation per frame)
@@ -348,21 +350,11 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
348
350
  robot_state=self.shared.vacuum_state,
349
351
  )
350
352
 
351
- # Update element map for robot position
352
- if (
353
- hasattr(self.shared, "element_map")
354
- and self.shared.element_map is not None
355
- ):
356
- update_element_map_with_robot(
357
- self.shared.element_map,
358
- robot_position,
359
- DrawableElement.ROBOT,
360
- )
361
353
  # Synchronize zooming state from ImageDraw to handler before auto-crop
362
354
  self.zooming = self.imd.img_h.zooming
363
355
 
364
356
  # Resize the image
365
- img_np_array = self.async_auto_trim_and_zoom_image(
357
+ img_np_array = self.auto_trim_and_zoom_image(
366
358
  img_np_array,
367
359
  colors["background"],
368
360
  int(self.shared.margins),
@@ -8,22 +8,22 @@ Version: v0.1.10
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import numpy as np
11
+ from dataclasses import asdict, dataclass, field
12
12
  from typing import (
13
- List,
14
- Sequence,
15
- TypeVar,
16
13
  Any,
17
- TypedDict,
18
- NotRequired,
19
14
  Literal,
15
+ NotRequired,
20
16
  Optional,
17
+ Sequence,
18
+ TypedDict,
19
+ TypeVar,
21
20
  )
22
21
 
23
- from dataclasses import dataclass, field, asdict
22
+ import numpy as np
24
23
 
25
24
  from .config.types import ImageSize, JsonType
26
25
 
26
+
27
27
  T = TypeVar("T")
28
28
 
29
29
  # --- Common Nested Structures ---
@@ -373,6 +373,11 @@ class ImageData:
373
373
  Else:
374
374
  (min_x_mm, min_y_mm, max_x_mm, max_y_mm)
375
375
  """
376
+
377
+ def to_mm(coord):
378
+ """Convert pixel coordinates to millimeters."""
379
+ return round(coord * pixel_size * 10)
380
+
376
381
  if not pixels:
377
382
  raise ValueError("Pixels list cannot be empty.")
378
383
 
@@ -393,7 +398,6 @@ class ImageData:
393
398
  min_y = min(min_y, y)
394
399
 
395
400
  if rand:
396
- to_mm = lambda v: v * pixel_size * 10
397
401
  return (to_mm(max_x), to_mm(max_y)), (to_mm(min_x), to_mm(min_y))
398
402
 
399
403
  return (
@@ -539,16 +543,18 @@ class RandImageData:
539
543
  return None
540
544
 
541
545
  @staticmethod
542
- def get_rrm_currently_cleaned_zones(json_data: JsonType) -> dict:
546
+ def get_rrm_currently_cleaned_zones(json_data: JsonType) -> list[dict[str, Any]]:
543
547
  """Get the currently cleaned zones from the json."""
544
548
  re_zones = json_data.get("currently_cleaned_zones", [])
545
549
  formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
546
550
  return formatted_zones
547
551
 
548
552
  @staticmethod
549
- def get_rrm_forbidden_zones(json_data: JsonType) -> dict:
553
+ def get_rrm_forbidden_zones(json_data: JsonType) -> list[dict[str, Any]]:
550
554
  """Get the forbidden zones from the json."""
551
- re_zones = json_data.get("forbidden_zones", [])
555
+ re_zones = json_data.get("forbidden_zones", []) + json_data.get(
556
+ "forbidden_mop_zones", []
557
+ )
552
558
  formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
553
559
  return formatted_zones
554
560