valetudo-map-parser 0.1.9b68__tar.gz → 0.1.9b70__tar.gz

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.
Files changed (27) hide show
  1. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/PKG-INFO +1 -1
  2. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/async_utils.py +11 -4
  3. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/drawable.py +39 -120
  4. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/shared.py +6 -4
  5. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/utils.py +56 -16
  6. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/hypfer_draw.py +9 -21
  7. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/hypfer_handler.py +26 -9
  8. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/map_data.py +22 -11
  9. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/rand256_handler.py +0 -2
  10. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/pyproject.toml +1 -1
  11. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/LICENSE +0 -0
  12. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/NOTICE.txt +0 -0
  13. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/README.md +0 -0
  14. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/__init__.py +0 -0
  15. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  16. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  17. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
  18. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/colors.py +0 -0
  19. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
  20. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
  21. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  22. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/rand256_parser.py +0 -0
  23. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/config/types.py +0 -0
  24. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
  25. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/py.typed +0 -0
  26. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
  27. {valetudo_map_parser-0.1.9b68 → valetudo_map_parser-0.1.9b70}/SCR/valetudo_map_parser/rooms_handler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b68
3
+ Version: 0.1.9b70
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -23,7 +23,9 @@ class AsyncNumPy:
23
23
  return await make_async(np.copy, array)
24
24
 
25
25
  @staticmethod
26
- async def async_full(shape: tuple, fill_value: Any, dtype: np.dtype = None) -> np.ndarray:
26
+ async def async_full(
27
+ shape: tuple, fill_value: Any, dtype: np.dtype = None
28
+ ) -> np.ndarray:
27
29
  """Async array creation with fill value."""
28
30
  return await make_async(np.full, shape, fill_value, dtype=dtype)
29
31
 
@@ -42,20 +44,25 @@ class AsyncPIL:
42
44
  return await make_async(Image.fromarray, array, mode)
43
45
 
44
46
  @staticmethod
45
- async def async_resize(image: Image.Image, size: tuple, resample: int = None) -> Image.Image:
47
+ async def async_resize(
48
+ image: Image.Image, size: tuple, resample: int = None
49
+ ) -> Image.Image:
46
50
  """Async image resizing."""
47
51
  if resample is None:
48
52
  resample = Image.LANCZOS
49
53
  return await make_async(image.resize, size, resample)
50
54
 
51
55
  @staticmethod
52
- async def async_save_to_bytes(image: Image.Image, format_type: str = "WEBP", **kwargs) -> bytes:
56
+ async def async_save_to_bytes(
57
+ image: Image.Image, format_type: str = "WEBP", **kwargs
58
+ ) -> bytes:
53
59
  """Async image saving to bytes."""
60
+
54
61
  def save_to_bytes():
55
62
  buffer = io.BytesIO()
56
63
  image.save(buffer, format=format_type, **kwargs)
57
64
  return buffer.getvalue()
58
-
65
+
59
66
  return await make_async(save_to_bytes)
60
67
 
61
68
 
@@ -12,8 +12,6 @@ from __future__ import annotations
12
12
 
13
13
  import logging
14
14
  import math
15
- import asyncio
16
- import inspect
17
15
 
18
16
  import numpy as np
19
17
  from PIL import ImageDraw, ImageFont
@@ -46,12 +44,8 @@ class Drawable:
46
44
  width: int, height: int, background_color: Color
47
45
  ) -> NumpyArray:
48
46
  """Create the empty background image NumPy array.
49
- Background color is specified as an RGBA tuple.
50
- Optimized: Uses np.empty + broadcast instead of np.full for better performance."""
51
- # Use np.empty + broadcast instead of np.full (avoids double initialization)
52
- img_array = np.empty((height, width, 4), dtype=np.uint8)
53
- img_array[:] = background_color # Broadcast color to all pixels efficiently
54
- return img_array
47
+ Background color is specified as an RGBA tuple."""
48
+ return np.full((height, width, 4), background_color, dtype=np.uint8)
55
49
 
56
50
  @staticmethod
57
51
  async def from_json_to_image(
@@ -158,8 +152,6 @@ class Drawable:
158
152
  It uses the rotation angle of the image to orient the flag.
159
153
  Includes color blending for better visual integration.
160
154
  """
161
- await asyncio.sleep(0) # Yield control
162
-
163
155
  # Check if coordinates are within bounds
164
156
  height, width = layer.shape[:2]
165
157
  x, y = center
@@ -331,12 +323,7 @@ class Drawable:
331
323
  Join the coordinates creating a continuous line (path).
332
324
  Optimized with vectorized operations for better performance.
333
325
  """
334
-
335
- # Handle case where arr might be a coroutine (shouldn't happen but let's be safe)
336
- if inspect.iscoroutine(arr):
337
- arr = await arr
338
-
339
- for i, coord in enumerate(coords):
326
+ for coord in coords:
340
327
  x0, y0 = coord[0]
341
328
  try:
342
329
  x1, y1 = coord[1]
@@ -353,10 +340,6 @@ class Drawable:
353
340
  # Use the optimized line drawing method
354
341
  arr = Drawable._line(arr, x0, y0, x1, y1, blended_color, width)
355
342
 
356
- # Yield control every 100 operations to prevent blocking
357
- if i % 100 == 0:
358
- await asyncio.sleep(0)
359
-
360
343
  return arr
361
344
 
362
345
  @staticmethod
@@ -501,120 +484,56 @@ class Drawable:
501
484
  async def zones(layers: NumpyArray, coordinates, color: Color) -> NumpyArray:
502
485
  """
503
486
  Draw the zones on the input layer with color blending.
504
- Optimized with parallel processing for better performance.
487
+ Optimized with NumPy vectorized operations for better performance.
505
488
  """
506
- await asyncio.sleep(0) # Yield control
507
-
508
489
  dot_radius = 1 # Number of pixels for the dot
509
490
  dot_spacing = 4 # Space between dots
510
491
 
511
- # Process zones in parallel if there are multiple zones
512
- if len(coordinates) > 1:
513
- # Create tasks for parallel zone processing
514
- zone_tasks = []
515
- for zone in coordinates:
516
- zone_tasks.append(Drawable._process_single_zone(layers.copy(), zone, color, dot_radius, dot_spacing))
517
-
518
- # Execute all zone processing tasks in parallel
519
- zone_results = await asyncio.gather(*zone_tasks, return_exceptions=True)
520
-
521
- # Merge results back into the main layer
522
- for result in zone_results:
523
- if not isinstance(result, Exception):
524
- # Simple overlay - pixels that are different from original get updated
525
- mask = result != layers
526
- layers[mask] = result[mask]
527
- else:
528
- # Single zone - process directly
529
- for zone in coordinates:
530
- points = zone["points"]
531
- min_x = max(0, min(points[::2]))
532
- max_x = min(layers.shape[1] - 1, max(points[::2]))
533
- min_y = max(0, min(points[1::2]))
534
- max_y = min(layers.shape[0] - 1, max(points[1::2]))
535
-
536
- # Skip if zone is outside the image
537
- if min_x >= max_x or min_y >= max_y:
538
- continue
539
-
540
- # Sample a point from the zone to get the background color
541
- # Use the center of the zone for sampling
542
- sample_x = (min_x + max_x) // 2
543
- sample_y = (min_y + max_y) // 2
544
-
545
- # Blend the color with the background color at the sample point
546
- if 0 <= sample_y < layers.shape[0] and 0 <= sample_x < layers.shape[1]:
547
- blended_color = ColorsManagement.sample_and_blend_color(
548
- layers, sample_x, sample_y, color
549
- )
550
- else:
551
- blended_color = color
552
-
553
- # Create a grid of dot centers
554
- x_centers = np.arange(min_x, max_x, dot_spacing)
555
- y_centers = np.arange(min_y, max_y, dot_spacing)
556
-
557
- # Draw dots at each grid point
558
- for y in y_centers:
559
- for x in x_centers:
560
- # Create a small mask for the dot
561
- y_min = max(0, y - dot_radius)
562
- y_max = min(layers.shape[0], y + dot_radius + 1)
563
- x_min = max(0, x - dot_radius)
564
- x_max = min(layers.shape[1], x + dot_radius + 1)
492
+ for zone in coordinates:
493
+ points = zone["points"]
494
+ min_x = max(0, min(points[::2]))
495
+ max_x = min(layers.shape[1] - 1, max(points[::2]))
496
+ min_y = max(0, min(points[1::2]))
497
+ max_y = min(layers.shape[0] - 1, max(points[1::2]))
565
498
 
566
- # Create coordinate arrays for the dot
567
- y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
568
-
569
- # Create a circular mask
570
- mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
571
-
572
- # Apply the color to the masked region
573
- layers[y_min:y_max, x_min:x_max][mask] = blended_color
574
-
575
- return layers
499
+ # Skip if zone is outside the image
500
+ if min_x >= max_x or min_y >= max_y:
501
+ continue
576
502
 
577
- @staticmethod
578
- async def _process_single_zone(layers: NumpyArray, zone, color: Color, dot_radius: int, dot_spacing: int) -> NumpyArray:
579
- """Process a single zone for parallel execution."""
580
- await asyncio.sleep(0) # Yield control
581
-
582
- points = zone["points"]
583
- min_x = max(0, min(points[::2]))
584
- max_x = min(layers.shape[1] - 1, max(points[::2]))
585
- min_y = max(0, min(points[1::2]))
586
- max_y = min(layers.shape[0] - 1, max(points[1::2]))
587
-
588
- # Skip if zone is outside the image
589
- if min_x >= max_x or min_y >= max_y:
590
- return layers
503
+ # Sample a point from the zone to get the background color
504
+ # Use the center of the zone for sampling
505
+ sample_x = (min_x + max_x) // 2
506
+ sample_y = (min_y + max_y) // 2
591
507
 
592
- # Sample a point from the zone to get the background color
593
- sample_x = (min_x + max_x) // 2
594
- sample_y = (min_y + max_y) // 2
508
+ # Blend the color with the background color at the sample point
509
+ if 0 <= sample_y < layers.shape[0] and 0 <= sample_x < layers.shape[1]:
510
+ blended_color = ColorsManagement.sample_and_blend_color(
511
+ layers, sample_x, sample_y, color
512
+ )
513
+ else:
514
+ blended_color = color
595
515
 
596
- # Blend the color with the background color at the sample point
597
- if 0 <= sample_y < layers.shape[0] and 0 <= sample_x < layers.shape[1]:
598
- blended_color = ColorsManagement.sample_and_blend_color(
599
- layers, sample_x, sample_y, color
600
- )
601
- else:
602
- blended_color = color
516
+ # Create a grid of dot centers
517
+ x_centers = np.arange(min_x, max_x, dot_spacing)
518
+ y_centers = np.arange(min_y, max_y, dot_spacing)
603
519
 
604
- # Create a dotted pattern within the zone
605
- for y in range(min_y, max_y + 1, dot_spacing):
606
- for x in range(min_x, max_x + 1, dot_spacing):
607
- if Drawable.point_inside(x, y, points):
608
- # Draw a small filled circle (dot) using vectorized operations
520
+ # Draw dots at each grid point
521
+ for y in y_centers:
522
+ for x in x_centers:
523
+ # Create a small mask for the dot
609
524
  y_min = max(0, y - dot_radius)
610
525
  y_max = min(layers.shape[0], y + dot_radius + 1)
611
526
  x_min = max(0, x - dot_radius)
612
527
  x_max = min(layers.shape[1], x + dot_radius + 1)
613
528
 
614
- if y_min < y_max and x_min < x_max:
615
- y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
616
- mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
617
- layers[y_min:y_max, x_min:x_max][mask] = blended_color
529
+ # Create coordinate arrays for the dot
530
+ y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
531
+
532
+ # Create a circular mask
533
+ mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
534
+
535
+ # Apply the color to the masked region
536
+ layers[y_min:y_max, x_min:x_max][mask] = blended_color
618
537
 
619
538
  return layers
620
539
 
@@ -1,12 +1,13 @@
1
1
  """
2
2
  Class Camera Shared.
3
3
  Keep the data between the modules.
4
- Version: v2024.12.0
4
+ Version: v0.1.9
5
5
  """
6
6
 
7
7
  import asyncio
8
8
  import logging
9
9
  from typing import List
10
+ from PIL import Image
10
11
 
11
12
  from .types import (
12
13
  ATTR_CALIBRATION_POINTS,
@@ -59,7 +60,10 @@ class CameraShared:
59
60
  self.rand256_active_zone: list = [] # Active zone for rand256
60
61
  self.is_rand: bool = False # MQTT rand data
61
62
  self._new_mqtt_message = False # New MQTT message
62
- self.last_image = PilPNG | None # Last image received
63
+ # Initialize last_image with default gray image (250x150 minimum)
64
+ self.last_image = Image.new(
65
+ "RGBA", (250, 150), (128, 128, 128, 255)
66
+ ) # Gray default image
63
67
  self.new_image: PilPNG | None = None # New image received
64
68
  self.binary_image: bytes | None = None # Current image in binary format
65
69
  self.image_last_updated: float = 0.0 # Last image update time
@@ -115,8 +119,6 @@ class CameraShared:
115
119
  self.skip_room_ids: List[str] = []
116
120
  self.device_info = None # Store the device_info
117
121
 
118
-
119
-
120
122
  def vacuum_bat_charged(self) -> bool:
121
123
  """Check if the vacuum is charging."""
122
124
  return (self.vacuum_state == "docked") and (int(self.vacuum_battery) < 100)
@@ -11,7 +11,7 @@ import numpy as np
11
11
  from PIL import Image, ImageOps
12
12
 
13
13
  from .drawable import Drawable
14
- from .drawable_elements import DrawableElement, DrawingConfig
14
+ from .drawable_elements import DrawingConfig
15
15
  from .enhanced_drawable import EnhancedDrawable
16
16
  from .types import (
17
17
  LOGGER,
@@ -80,10 +80,11 @@ class BaseHandler:
80
80
  """Return the robot position."""
81
81
  return self.robot_pos
82
82
 
83
- async def async_get_pil_image(
83
+ async def async_get_image(
84
84
  self,
85
85
  m_json: dict | None,
86
86
  destinations: list | None = None,
87
+ bytes_format: bool = False,
87
88
  ) -> PilPNG | None:
88
89
  """
89
90
  Unified async function to get PIL image from JSON data for both Hypfer and Rand256 handlers.
@@ -96,50 +97,89 @@ class BaseHandler:
96
97
 
97
98
  @param m_json: The JSON data to use to draw the image
98
99
  @param destinations: MQTT destinations for labels (used by Rand256)
100
+ @param bytes_format: If True, also convert to PNG bytes and store in shared.binary_image
99
101
  @return: PIL Image or None
100
102
  """
101
103
  try:
102
104
  # Backup current image to last_image before processing new one
103
- if hasattr(self.shared, 'new_image') and self.shared.new_image is not None:
105
+ if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
104
106
  self.shared.last_image = self.shared.new_image
105
107
 
106
108
  # Call the appropriate handler method based on handler type
107
- if hasattr(self, 'get_image_from_rrm'):
109
+ if hasattr(self, "get_image_from_rrm"):
108
110
  # This is a Rand256 handler
109
111
  new_image = await self.get_image_from_rrm(
110
112
  m_json=m_json,
111
113
  destinations=destinations,
112
- return_webp=False # Always return PIL Image
114
+ return_webp=False, # Always return PIL Image
113
115
  )
114
- elif hasattr(self, 'async_get_image_from_json'):
116
+ elif hasattr(self, "async_get_image_from_json"):
115
117
  # This is a Hypfer handler
116
118
  new_image = await self.async_get_image_from_json(
117
119
  m_json=m_json,
118
- return_webp=False # Always return PIL Image
120
+ return_webp=False, # Always return PIL Image
119
121
  )
120
122
  else:
121
- LOGGER.warning("%s: Handler type not recognized for async_get_pil_image", self.file_name)
122
- return None
123
+ LOGGER.warning(
124
+ "%s: Handler type not recognized for async_get_image",
125
+ self.file_name,
126
+ )
127
+ return (
128
+ self.shared.last_image
129
+ if hasattr(self.shared, "last_image")
130
+ else None
131
+ )
123
132
 
124
133
  # Store the new image in shared data
125
134
  if new_image is not None:
126
135
  self.shared.new_image = new_image
136
+
137
+ # Convert to binary (PNG bytes) if requested
138
+ if bytes_format:
139
+ try:
140
+ png_buffer = io.BytesIO()
141
+ new_image.save(png_buffer, format="PNG")
142
+ self.shared.binary_image = png_buffer.getvalue()
143
+ png_buffer.close()
144
+ LOGGER.debug(
145
+ "%s: Binary image conversion completed", self.file_name
146
+ )
147
+ except Exception as e:
148
+ LOGGER.warning(
149
+ "%s: Failed to convert image to binary: %s",
150
+ self.file_name,
151
+ str(e),
152
+ )
153
+ self.shared.binary_image = None
154
+ else:
155
+ self.shared.binary_image = None
156
+
127
157
  # Update the timestamp with current datetime
128
158
  self.shared.image_last_updated = datetime.datetime.now().timestamp()
129
- LOGGER.debug("%s: Image processed and stored in shared data", self.file_name)
159
+ LOGGER.debug(
160
+ "%s: Image processed and stored in shared data", self.file_name
161
+ )
162
+ return new_image
130
163
  else:
131
- LOGGER.warning("%s: Failed to generate image from JSON data", self.file_name)
132
-
133
- return new_image
164
+ LOGGER.warning(
165
+ "%s: Failed to generate image from JSON data", self.file_name
166
+ )
167
+ return (
168
+ self.shared.last_image
169
+ if hasattr(self.shared, "last_image")
170
+ else None
171
+ )
134
172
 
135
173
  except Exception as e:
136
174
  LOGGER.error(
137
- "%s: Error in async_get_pil_image: %s",
175
+ "%s: Error in async_get_image: %s",
138
176
  self.file_name,
139
177
  str(e),
140
- exc_info=True
178
+ exc_info=True,
179
+ )
180
+ return (
181
+ self.shared.last_image if hasattr(self.shared, "last_image") else None
141
182
  )
142
- return None
143
183
 
144
184
  def get_charger_position(self) -> ChargerPosition | None:
145
185
  """Return the charger position."""
@@ -1,12 +1,11 @@
1
1
  """
2
2
  Image Draw Class for Valetudo Hypfer Image Handling.
3
3
  This class is used to simplify the ImageHandler class.
4
- Version: 2024.07.2
4
+ Version: 0.1.9
5
5
  """
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import asyncio
10
9
  import logging
11
10
 
12
11
  from .config.drawable_elements import DrawableElement
@@ -337,39 +336,30 @@ class ImageDraw:
337
336
  _LOGGER.info("%s: Got zones.", self.file_name)
338
337
 
339
338
  if zone_clean:
340
- # Prepare zone drawing tasks for parallel execution
341
- zone_tasks = []
339
+ # Process zones sequentially to avoid memory-intensive array copies
340
+ # This is more memory-efficient than parallel processing with copies
342
341
 
343
342
  # Active zones
344
343
  zones_active = zone_clean.get("active_zone")
345
344
  if zones_active:
346
- zone_tasks.append(
347
- self.img_h.draw.zones(np_array.copy(), zones_active, color_zone_clean)
345
+ np_array = await self.img_h.draw.zones(
346
+ np_array, zones_active, color_zone_clean
348
347
  )
349
348
 
350
349
  # No-go zones
351
350
  no_go_zones = zone_clean.get("no_go_area")
352
351
  if no_go_zones:
353
- zone_tasks.append(
354
- self.img_h.draw.zones(np_array.copy(), no_go_zones, color_no_go)
352
+ np_array = await self.img_h.draw.zones(
353
+ np_array, no_go_zones, color_no_go
355
354
  )
356
355
 
357
356
  # No-mop zones
358
357
  no_mop_zones = zone_clean.get("no_mop_area")
359
358
  if no_mop_zones:
360
- zone_tasks.append(
361
- self.img_h.draw.zones(np_array.copy(), no_mop_zones, color_no_go)
359
+ np_array = await self.img_h.draw.zones(
360
+ np_array, no_mop_zones, color_no_go
362
361
  )
363
362
 
364
- # Execute all zone drawing tasks in parallel
365
- if zone_tasks:
366
- zone_results = await asyncio.gather(*zone_tasks)
367
- # Merge results back into the main array
368
- for result in zone_results:
369
- # Simple overlay - in practice you might want more sophisticated blending
370
- mask = result != np_array
371
- np_array[mask] = result[mask]
372
-
373
363
  return np_array
374
364
 
375
365
  async def async_draw_virtual_walls(
@@ -439,7 +429,6 @@ class ImageDraw:
439
429
  def _check_active_zone_and_set_zooming(self) -> None:
440
430
  """Helper function to check active zones and set zooming state."""
441
431
  if self.img_h.active_zones and self.img_h.robot_in_room:
442
-
443
432
  segment_id = str(self.img_h.robot_in_room["id"])
444
433
  room_store = RoomStore(self.file_name)
445
434
  room_keys = list(room_store.get_rooms().keys())
@@ -615,7 +604,6 @@ class ImageDraw:
615
604
 
616
605
  # Handle active zones - Map segment ID to active_zones position
617
606
  if self.img_h.active_zones:
618
-
619
607
  segment_id = str(self.img_h.robot_in_room["id"])
620
608
  room_store = RoomStore(self.file_name)
621
609
  room_keys = list(room_store.get_rooms().keys())
@@ -8,6 +8,7 @@ Version: 0.1.9
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
+ import numpy as np
11
12
 
12
13
  from PIL import Image
13
14
 
@@ -58,6 +59,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
58
59
  self.go_to = None # vacuum go to data
59
60
  self.img_hash = None # hash of the image calculated to check differences.
60
61
  self.img_base_layer = None # numpy array store the map base layer.
62
+ self.img_work_layer = None # persistent working buffer to avoid per-frame allocations
61
63
  self.active_zones = None # vacuum active zones.
62
64
  self.svg_wait = False # SVG image creation wait.
63
65
  self.imd = ImDraw(self) # Image Draw class.
@@ -210,14 +212,12 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
210
212
  ) % 16 # Increment room_id even if we skip
211
213
  continue
212
214
 
213
- # Check if this is a wall layer and if walls are enabled
215
+ # Draw the layer ONLY if enabled
214
216
  is_wall_layer = layer_type == "wall"
215
217
  if is_wall_layer:
216
- if not self.drawing_config.is_enabled(
217
- DrawableElement.WALL
218
- ):
219
- pass
220
-
218
+ # Skip walls entirely if disabled
219
+ if not self.drawing_config.is_enabled(DrawableElement.WALL):
220
+ continue
221
221
  # Draw the layer
222
222
  (
223
223
  room_id,
@@ -273,6 +273,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
273
273
  LOGGER.info("%s: Completed base Layers", self.file_name)
274
274
  # Copy the new array in base layer.
275
275
  self.img_base_layer = await self.async_copy_array(img_np_array)
276
+
277
+
276
278
  self.shared.frame_number = self.frame_number
277
279
  self.frame_number += 1
278
280
  if (self.frame_number >= self.max_frames) or (
@@ -285,8 +287,17 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
285
287
  str(self.json_id),
286
288
  str(self.frame_number),
287
289
  )
288
- # Copy the base layer to the new image.
289
- img_np_array = await self.async_copy_array(self.img_base_layer)
290
+ # Ensure persistent working buffer exists and matches base (allocate only when needed)
291
+ if (
292
+ self.img_work_layer is None
293
+ or self.img_work_layer.shape != self.img_base_layer.shape
294
+ or self.img_work_layer.dtype != self.img_base_layer.dtype
295
+ ):
296
+ self.img_work_layer = np.empty_like(self.img_base_layer)
297
+
298
+ # Copy the base layer into the persistent working buffer (no new allocation per frame)
299
+ np.copyto(self.img_work_layer, self.img_base_layer)
300
+ img_np_array = self.img_work_layer
290
301
 
291
302
  # Prepare parallel data extraction tasks
292
303
  data_tasks = []
@@ -301,11 +312,17 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
301
312
 
302
313
  # Prepare path data extraction
303
314
  path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
304
- LOGGER.info("%s: PATH element enabled: %s", self.file_name, path_enabled)
315
+ LOGGER.info(
316
+ "%s: PATH element enabled: %s", self.file_name, path_enabled
317
+ )
305
318
  if path_enabled:
306
319
  LOGGER.info("%s: Drawing path", self.file_name)
307
320
  data_tasks.append(self._prepare_path_data(m_json))
308
321
 
322
+ # Await all data preparation tasks if any were created
323
+ if data_tasks:
324
+ await asyncio.gather(*data_tasks)
325
+
309
326
  # Process drawing operations sequentially (since they modify the same array)
310
327
  # Draw zones if enabled
311
328
  if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
@@ -19,14 +19,14 @@ class ImageData:
19
19
  @staticmethod
20
20
  def sublist(lst, n):
21
21
  """Split a list into n chunks of specified size."""
22
- return [lst[i: i + n] for i in range(0, len(lst), n)]
22
+ return [lst[i : i + n] for i in range(0, len(lst), n)]
23
23
 
24
24
  @staticmethod
25
25
  def sublist_join(lst, n):
26
26
  """Join the lists in a unique list of n elements."""
27
27
  arr = np.array(lst)
28
28
  num_windows = len(lst) - n + 1
29
- result = [arr[i: i + n].tolist() for i in range(num_windows)]
29
+ result = [arr[i : i + n].tolist() for i in range(num_windows)]
30
30
  return result
31
31
 
32
32
  @staticmethod
@@ -39,15 +39,19 @@ class ImageData:
39
39
  points = obstacle.get("points", [])
40
40
  image_id = obstacle.get("metaData", {}).get("id")
41
41
  if label and points:
42
- obstacle_positions.append({
43
- "label": label,
44
- "points": {"x": points[0], "y": points[1]},
45
- "id": image_id,
46
- })
42
+ obstacle_positions.append(
43
+ {
44
+ "label": label,
45
+ "points": {"x": points[0], "y": points[1]},
46
+ "id": image_id,
47
+ }
48
+ )
47
49
  return obstacle_positions
48
50
 
49
51
  @staticmethod
50
- def find_layers(json_obj: JsonType, layer_dict: dict, active_list: list) -> tuple[dict, list]:
52
+ def find_layers(
53
+ json_obj: JsonType, layer_dict: dict, active_list: list
54
+ ) -> tuple[dict, list]:
51
55
  """Find the layers in the json object."""
52
56
  layer_dict = {} if layer_dict is None else layer_dict
53
57
  active_list = [] if active_list is None else active_list
@@ -56,7 +60,9 @@ class ImageData:
56
60
  layer_type = json_obj.get("type")
57
61
  active_type = json_obj.get("metaData")
58
62
  if layer_type:
59
- layer_dict.setdefault(layer_type, []).append(json_obj.get("compressedPixels", []))
63
+ layer_dict.setdefault(layer_type, []).append(
64
+ json_obj.get("compressedPixels", [])
65
+ )
60
66
  if layer_type == "segment":
61
67
  active_list.append(int(active_type.get("active", 0)))
62
68
  for value in json_obj.values():
@@ -121,7 +127,10 @@ class ImageData:
121
127
 
122
128
  def _recursive(obj):
123
129
  if isinstance(obj, dict):
124
- if obj.get("__class") == "LineMapEntity" and obj.get("type") == "virtual_wall":
130
+ if (
131
+ obj.get("__class") == "LineMapEntity"
132
+ and obj.get("type") == "virtual_wall"
133
+ ):
125
134
  walls.append(obj["points"])
126
135
  for value in obj.values():
127
136
  _recursive(value)
@@ -133,7 +142,9 @@ class ImageData:
133
142
  return walls
134
143
 
135
144
  @staticmethod
136
- async def async_get_rooms_coordinates(pixels: list, pixel_size: int = 5, rand: bool = False) -> tuple:
145
+ async def async_get_rooms_coordinates(
146
+ pixels: list, pixel_size: int = 5, rand: bool = False
147
+ ) -> tuple:
137
148
  """Extract the room coordinates from the vacuum pixels data."""
138
149
  df = pd.DataFrame(pixels, columns=["x", "y", "length"])
139
150
  if rand:
@@ -672,5 +672,3 @@ class ReImageHandler(BaseHandler, AutoCrop):
672
672
  async def async_copy_array(self, original_array):
673
673
  """Copy the array using async utilities."""
674
674
  return await AsyncNumPy.async_copy(original_array)
675
-
676
-
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9b68"
3
+ version = "0.1.9b70"
4
4
  description = "A Python library to parse Valetudo map data returning a PIL Image object."
5
5
  authors = ["Sandro Cantarella <gsca075@gmail.com>"]
6
6
  license = "Apache-2.0"