valetudo-map-parser 0.1.9b68__py3-none-any.whl → 0.1.9b70__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/async_utils.py +11 -4
- valetudo_map_parser/config/drawable.py +39 -120
- valetudo_map_parser/config/shared.py +6 -4
- valetudo_map_parser/config/utils.py +56 -16
- valetudo_map_parser/hypfer_draw.py +9 -21
- valetudo_map_parser/hypfer_handler.py +26 -9
- valetudo_map_parser/map_data.py +22 -11
- valetudo_map_parser/rand256_handler.py +0 -2
- {valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/METADATA +1 -1
- {valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/RECORD +13 -13
- {valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/WHEEL +0 -0
@@ -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(
|
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(
|
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(
|
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
|
-
|
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
|
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
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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
|
-
|
567
|
-
|
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
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
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
|
-
|
593
|
-
|
594
|
-
|
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
|
-
|
597
|
-
|
598
|
-
|
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
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
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
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
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:
|
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
|
-
|
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
|
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
|
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,
|
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,
|
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,
|
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(
|
122
|
-
|
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(
|
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(
|
132
|
-
|
133
|
-
|
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
|
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:
|
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
|
-
#
|
341
|
-
|
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
|
-
|
347
|
-
|
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
|
-
|
354
|
-
|
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
|
-
|
361
|
-
|
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
|
-
#
|
215
|
+
# Draw the layer ONLY if enabled
|
214
216
|
is_wall_layer = layer_type == "wall"
|
215
217
|
if is_wall_layer:
|
216
|
-
if
|
217
|
-
|
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
|
-
#
|
289
|
-
|
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(
|
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):
|
valetudo_map_parser/map_data.py
CHANGED
@@ -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
|
-
|
44
|
-
|
45
|
-
|
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(
|
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(
|
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
|
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(
|
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:
|
@@ -1,27 +1,27 @@
|
|
1
1
|
valetudo_map_parser/__init__.py,sha256=XO_eJwFDyU7hXJ4tAa2zY-n-SM2_kmIGMWDKY3GcauY,1163
|
2
2
|
valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
|
3
|
-
valetudo_map_parser/config/async_utils.py,sha256=
|
3
|
+
valetudo_map_parser/config/async_utils.py,sha256=e1j9uTtg4dhPVWvB2_XgqaH4aeSjRAPz-puRMbGoOs8,3204
|
4
4
|
valetudo_map_parser/config/auto_crop.py,sha256=Aes7vfv4z8ihYvGaH5Nryj6Y9mHDerZLIeyvePjf9aQ,19259
|
5
5
|
valetudo_map_parser/config/color_utils.py,sha256=nXD6WeNmdFdoMxPDW-JFpjnxJSaZR1jX-ouNfrx6zvE,4502
|
6
6
|
valetudo_map_parser/config/colors.py,sha256=DG-oPQoN5gsnwDbEsuFr8a0hRCxmbFHObWa4_5pr-70,29910
|
7
|
-
valetudo_map_parser/config/drawable.py,sha256=
|
7
|
+
valetudo_map_parser/config/drawable.py,sha256=2MeVHXqZuVuJk3eerMJYGwo25rVetHx3xB_vxecEFOQ,34168
|
8
8
|
valetudo_map_parser/config/drawable_elements.py,sha256=o-5oiXmfqPwNQLzKIhkEcZD_A47rIU9E0CqKgWipxgc,11516
|
9
9
|
valetudo_map_parser/config/enhanced_drawable.py,sha256=QlGxlUMVgECUXPtFwIslyjubWxQuhIixsRymWV3lEvk,12586
|
10
10
|
valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
|
11
11
|
valetudo_map_parser/config/rand256_parser.py,sha256=LU3y7XvRRQxVen9iwom0dOaDnJJvhZdg97NqOYRZFas,16279
|
12
|
-
valetudo_map_parser/config/shared.py,sha256=
|
12
|
+
valetudo_map_parser/config/shared.py,sha256=98CgGDY0tbc5BSg2TIHbGcDFZZ2acgIYnoPjAwENmBU,12885
|
13
13
|
valetudo_map_parser/config/types.py,sha256=saL7pULKAdTRQ_ShR2arT8IV472e9MBC_SohTthlGp8,17567
|
14
|
-
valetudo_map_parser/config/utils.py,sha256=
|
15
|
-
valetudo_map_parser/hypfer_draw.py,sha256=
|
16
|
-
valetudo_map_parser/hypfer_handler.py,sha256=
|
14
|
+
valetudo_map_parser/config/utils.py,sha256=lRLvQbqCQ44knnIJr-UAlkEOO1d14mFCmHKDor989VE,35468
|
15
|
+
valetudo_map_parser/hypfer_draw.py,sha256=mMA0MLmV3Man47VUmj823xwyHprJJmb0nXxmutRNK58,28976
|
16
|
+
valetudo_map_parser/hypfer_handler.py,sha256=PRiT7X7U9nbN3uvjSl1HKyBKN38CrM9_Z2Y-RNB3nJ8,23968
|
17
17
|
valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
|
18
|
-
valetudo_map_parser/map_data.py,sha256=
|
18
|
+
valetudo_map_parser/map_data.py,sha256=Flq5t9QQQiD5ylObIniHhPP1VB7VhNoMcMeJrOey3Go,17433
|
19
19
|
valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
-
valetudo_map_parser/rand256_handler.py,sha256=
|
20
|
+
valetudo_map_parser/rand256_handler.py,sha256=daaSQ5ktMUYMnYxJkjS75UdBchpXVZ58HIomwHBFivs,27651
|
21
21
|
valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
|
22
22
|
valetudo_map_parser/rooms_handler.py,sha256=ovqQtAjauAqwUNPR0aX27P2zhheQmqfaFhDE3_AwYWk,17821
|
23
|
-
valetudo_map_parser-0.1.
|
24
|
-
valetudo_map_parser-0.1.
|
25
|
-
valetudo_map_parser-0.1.
|
26
|
-
valetudo_map_parser-0.1.
|
27
|
-
valetudo_map_parser-0.1.
|
23
|
+
valetudo_map_parser-0.1.9b70.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
|
24
|
+
valetudo_map_parser-0.1.9b70.dist-info/METADATA,sha256=oadNkSUgJDO-yswCRC2dow27dI0ntUsKH10Z5OURuic,3353
|
25
|
+
valetudo_map_parser-0.1.9b70.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
|
26
|
+
valetudo_map_parser-0.1.9b70.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
27
|
+
valetudo_map_parser-0.1.9b70.dist-info/RECORD,,
|
File without changes
|
{valetudo_map_parser-0.1.9b68.dist-info → valetudo_map_parser-0.1.9b70.dist-info}/NOTICE.txt
RENAMED
File without changes
|
File without changes
|