valetudo-map-parser 0.1.9b60__tar.gz → 0.1.9b62__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.
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/PKG-INFO +2 -1
- valetudo_map_parser-0.1.9b62/SCR/valetudo_map_parser/config/async_utils.py +89 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/auto_crop.py +4 -3
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/drawable.py +120 -39
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/shared.py +9 -1
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/types.py +2 -1
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/hypfer_draw.py +32 -24
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/hypfer_handler.py +55 -17
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/map_data.py +51 -90
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/pyproject.toml +2 -1
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/README.md +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/colors.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/rand256_parser.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/config/utils.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/rand256_handler.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
- {valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/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.
|
3
|
+
Version: 0.1.9b62
|
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
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
13
13
|
Requires-Dist: Pillow (>=10.3.0)
|
14
14
|
Requires-Dist: numpy (>=1.26.4)
|
15
|
+
Requires-Dist: pandas (>=2.3.0)
|
15
16
|
Requires-Dist: scipy (>=1.12.0)
|
16
17
|
Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
|
17
18
|
Project-URL: Changelog, https://github.com/sca075/Python-package-valetudo-map-parser/releases
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"""Async utility functions for making NumPy and PIL operations truly async."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import io
|
5
|
+
from typing import Any, Callable
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
from numpy import rot90
|
9
|
+
from PIL import Image
|
10
|
+
|
11
|
+
|
12
|
+
async def make_async(func: Callable, *args, **kwargs) -> Any:
|
13
|
+
"""Convert a synchronous function to async by yielding control to the event loop."""
|
14
|
+
await asyncio.sleep(0)
|
15
|
+
result = func(*args, **kwargs)
|
16
|
+
await asyncio.sleep(0)
|
17
|
+
return result
|
18
|
+
|
19
|
+
|
20
|
+
class AsyncNumPy:
|
21
|
+
"""Async wrappers for NumPy operations that yield control to the event loop."""
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
async def async_copy(array: np.ndarray) -> np.ndarray:
|
25
|
+
"""Async array copying."""
|
26
|
+
return await make_async(np.copy, array)
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
async def async_full(shape: tuple, fill_value: Any, dtype: np.dtype = None) -> np.ndarray:
|
30
|
+
"""Async array creation with fill value."""
|
31
|
+
return await make_async(np.full, shape, fill_value, dtype=dtype)
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
async def async_rot90(array: np.ndarray, k: int = 1) -> np.ndarray:
|
35
|
+
"""Async array rotation."""
|
36
|
+
return await make_async(rot90, array, k)
|
37
|
+
|
38
|
+
|
39
|
+
class AsyncPIL:
|
40
|
+
"""Async wrappers for PIL operations that yield control to the event loop."""
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
async def async_fromarray(array: np.ndarray, mode: str = "RGBA") -> Image.Image:
|
44
|
+
"""Async PIL Image creation from NumPy array."""
|
45
|
+
return await make_async(Image.fromarray, array, mode)
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
async def async_resize(image: Image.Image, size: tuple, resample: int = None) -> Image.Image:
|
49
|
+
"""Async image resizing."""
|
50
|
+
if resample is None:
|
51
|
+
resample = Image.LANCZOS
|
52
|
+
return await make_async(image.resize, size, resample)
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
async def async_save_to_bytes(image: Image.Image, format_type: str = "WEBP", **kwargs) -> bytes:
|
56
|
+
"""Async image saving to bytes."""
|
57
|
+
def save_to_bytes():
|
58
|
+
buffer = io.BytesIO()
|
59
|
+
image.save(buffer, format=format_type, **kwargs)
|
60
|
+
return buffer.getvalue()
|
61
|
+
|
62
|
+
return await make_async(save_to_bytes)
|
63
|
+
|
64
|
+
|
65
|
+
class AsyncParallel:
|
66
|
+
"""Helper functions for parallel processing with asyncio.gather()."""
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
async def parallel_data_preparation(*tasks):
|
70
|
+
"""Execute multiple data preparation tasks in parallel."""
|
71
|
+
return await asyncio.gather(*tasks, return_exceptions=True)
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
async def parallel_array_operations(base_array: np.ndarray, operations: list):
|
75
|
+
"""Execute multiple array operations in parallel on copies of the base array."""
|
76
|
+
|
77
|
+
# Create tasks for parallel execution
|
78
|
+
tasks = []
|
79
|
+
for operation_func, *args in operations:
|
80
|
+
# Each operation works on a copy of the base array
|
81
|
+
array_copy = await AsyncNumPy.async_copy(base_array)
|
82
|
+
tasks.append(operation_func(array_copy, *args))
|
83
|
+
|
84
|
+
# Execute all operations in parallel
|
85
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
86
|
+
|
87
|
+
# Filter out exceptions and return successful results
|
88
|
+
successful_results = [r for r in results if not isinstance(r, Exception)]
|
89
|
+
return successful_results
|
@@ -9,6 +9,7 @@ import numpy as np
|
|
9
9
|
from numpy import rot90
|
10
10
|
from scipy import ndimage
|
11
11
|
|
12
|
+
from .async_utils import AsyncNumPy, make_async
|
12
13
|
from .types import Color, NumpyArray, TrimCropData, TrimsData
|
13
14
|
from .utils import BaseHandler
|
14
15
|
|
@@ -364,7 +365,7 @@ class AutoCrop:
|
|
364
365
|
) -> NumpyArray:
|
365
366
|
"""Rotate the image and return the new array."""
|
366
367
|
if rotate == 90:
|
367
|
-
rotated =
|
368
|
+
rotated = await AsyncNumPy.async_rot90(trimmed)
|
368
369
|
self.crop_area = [
|
369
370
|
self.trim_left,
|
370
371
|
self.trim_up,
|
@@ -372,10 +373,10 @@ class AutoCrop:
|
|
372
373
|
self.trim_down,
|
373
374
|
]
|
374
375
|
elif rotate == 180:
|
375
|
-
rotated =
|
376
|
+
rotated = await AsyncNumPy.async_rot90(trimmed, 2)
|
376
377
|
self.crop_area = self.auto_crop
|
377
378
|
elif rotate == 270:
|
378
|
-
rotated =
|
379
|
+
rotated = await AsyncNumPy.async_rot90(trimmed, 3)
|
379
380
|
self.crop_area = [
|
380
381
|
self.trim_left,
|
381
382
|
self.trim_up,
|
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|
12
12
|
|
13
13
|
import logging
|
14
14
|
import math
|
15
|
+
import asyncio
|
16
|
+
import inspect
|
15
17
|
|
16
18
|
import numpy as np
|
17
19
|
from PIL import ImageDraw, ImageFont
|
@@ -44,8 +46,12 @@ class Drawable:
|
|
44
46
|
width: int, height: int, background_color: Color
|
45
47
|
) -> NumpyArray:
|
46
48
|
"""Create the empty background image NumPy array.
|
47
|
-
Background color is specified as an RGBA tuple.
|
48
|
-
|
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
|
49
55
|
|
50
56
|
@staticmethod
|
51
57
|
async def from_json_to_image(
|
@@ -152,6 +158,8 @@ class Drawable:
|
|
152
158
|
It uses the rotation angle of the image to orient the flag.
|
153
159
|
Includes color blending for better visual integration.
|
154
160
|
"""
|
161
|
+
await asyncio.sleep(0) # Yield control
|
162
|
+
|
155
163
|
# Check if coordinates are within bounds
|
156
164
|
height, width = layer.shape[:2]
|
157
165
|
x, y = center
|
@@ -323,7 +331,12 @@ class Drawable:
|
|
323
331
|
Join the coordinates creating a continuous line (path).
|
324
332
|
Optimized with vectorized operations for better performance.
|
325
333
|
"""
|
326
|
-
|
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):
|
327
340
|
x0, y0 = coord[0]
|
328
341
|
try:
|
329
342
|
x1, y1 = coord[1]
|
@@ -340,6 +353,10 @@ class Drawable:
|
|
340
353
|
# Use the optimized line drawing method
|
341
354
|
arr = Drawable._line(arr, x0, y0, x1, y1, blended_color, width)
|
342
355
|
|
356
|
+
# Yield control every 100 operations to prevent blocking
|
357
|
+
if i % 100 == 0:
|
358
|
+
await asyncio.sleep(0)
|
359
|
+
|
343
360
|
return arr
|
344
361
|
|
345
362
|
@staticmethod
|
@@ -484,56 +501,120 @@ class Drawable:
|
|
484
501
|
async def zones(layers: NumpyArray, coordinates, color: Color) -> NumpyArray:
|
485
502
|
"""
|
486
503
|
Draw the zones on the input layer with color blending.
|
487
|
-
Optimized with
|
504
|
+
Optimized with parallel processing for better performance.
|
488
505
|
"""
|
506
|
+
await asyncio.sleep(0) # Yield control
|
507
|
+
|
489
508
|
dot_radius = 1 # Number of pixels for the dot
|
490
509
|
dot_spacing = 4 # Space between dots
|
491
510
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
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
|
498
544
|
|
499
|
-
|
500
|
-
|
501
|
-
|
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
|
502
552
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
sample_y = (min_y + max_y) // 2
|
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)
|
507
556
|
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
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)
|
515
565
|
|
516
|
-
|
517
|
-
|
518
|
-
y_centers = np.arange(min_y, max_y, dot_spacing)
|
566
|
+
# Create coordinate arrays for the dot
|
567
|
+
y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
|
519
568
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
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
|
576
|
+
|
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
|
591
|
+
|
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
|
595
|
+
|
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
|
603
|
+
|
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
|
524
609
|
y_min = max(0, y - dot_radius)
|
525
610
|
y_max = min(layers.shape[0], y + dot_radius + 1)
|
526
611
|
x_min = max(0, x - dot_radius)
|
527
612
|
x_max = min(layers.shape[1], x + dot_radius + 1)
|
528
613
|
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
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
|
537
618
|
|
538
619
|
return layers
|
539
620
|
|
@@ -18,6 +18,7 @@ from .types import (
|
|
18
18
|
ATTR_ROTATE,
|
19
19
|
ATTR_SNAPSHOT,
|
20
20
|
ATTR_VACUUM_BATTERY,
|
21
|
+
ATTR_VACUUM_CHARGING,
|
21
22
|
ATTR_VACUUM_JSON_ID,
|
22
23
|
ATTR_VACUUM_POSITION,
|
23
24
|
ATTR_VACUUM_STATUS,
|
@@ -60,7 +61,7 @@ class CameraShared:
|
|
60
61
|
self.last_image = None # Last image received
|
61
62
|
self.current_image = None # Current image
|
62
63
|
self.binary_image = None # Current image in binary format
|
63
|
-
self.image_format = "
|
64
|
+
self.image_format = "image/pil" # Image format
|
64
65
|
self.image_size = None # Image size
|
65
66
|
self.image_auto_zoom: bool = False # Auto zoom image
|
66
67
|
self.image_zoom_lock_ratio: bool = True # Zoom lock ratio
|
@@ -112,6 +113,12 @@ class CameraShared:
|
|
112
113
|
self.skip_room_ids: List[str] = []
|
113
114
|
self.device_info = None # Store the device_info
|
114
115
|
|
116
|
+
|
117
|
+
|
118
|
+
def _state_charging(self) -> bool:
|
119
|
+
"""Check if the vacuum is charging."""
|
120
|
+
return self.vacuum_state == "charging"
|
121
|
+
|
115
122
|
@staticmethod
|
116
123
|
def _compose_obstacle_links(vacuum_host_ip: str, obstacles: list) -> list | None:
|
117
124
|
"""
|
@@ -186,6 +193,7 @@ class CameraShared:
|
|
186
193
|
attrs = {
|
187
194
|
ATTR_CAMERA_MODE: self.camera_mode,
|
188
195
|
ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
|
196
|
+
ATTR_VACUUM_CHARGING: self._state_charging(),
|
189
197
|
ATTR_VACUUM_POSITION: self.current_room,
|
190
198
|
ATTR_VACUUM_STATUS: self.vacuum_state,
|
191
199
|
ATTR_VACUUM_JSON_ID: self.vac_json_id,
|
@@ -568,7 +568,8 @@ ALPHA_ROOM_15 = "alpha_room_15"
|
|
568
568
|
|
569
569
|
""" Constants for the attribute keys """
|
570
570
|
ATTR_FRIENDLY_NAME = "friendly_name"
|
571
|
-
ATTR_VACUUM_BATTERY = "
|
571
|
+
ATTR_VACUUM_BATTERY = "battery"
|
572
|
+
ATTR_VACUUM_CHARGING = "charging"
|
572
573
|
ATTR_VACUUM_POSITION = "vacuum_position"
|
573
574
|
ATTR_VACUUM_TOPIC = "vacuum_topic"
|
574
575
|
ATTR_VACUUM_STATUS = "vacuum_status"
|
{valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/hypfer_draw.py
RENAMED
@@ -6,10 +6,11 @@ Version: 2024.07.2
|
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
+
import asyncio
|
9
10
|
import logging
|
10
11
|
|
11
12
|
from .config.drawable_elements import DrawableElement
|
12
|
-
from .config.types import Color, JsonType, NumpyArray, RobotPosition
|
13
|
+
from .config.types import Color, JsonType, NumpyArray, RobotPosition, RoomStore
|
13
14
|
|
14
15
|
|
15
16
|
_LOGGER = logging.getLogger(__name__)
|
@@ -92,7 +93,7 @@ class ImageDraw:
|
|
92
93
|
pixel_size,
|
93
94
|
disabled_rooms=None,
|
94
95
|
):
|
95
|
-
"""Draw the base layer of the map.
|
96
|
+
"""Draw the base layer of the map with parallel processing for rooms.
|
96
97
|
|
97
98
|
Args:
|
98
99
|
img_np_array: The image array to draw on
|
@@ -108,6 +109,7 @@ class ImageDraw:
|
|
108
109
|
"""
|
109
110
|
room_id = 0
|
110
111
|
|
112
|
+
# Sequential processing for rooms/segments (dependencies require this)
|
111
113
|
for compressed_pixels in compressed_pixels_list:
|
112
114
|
pixels = self.img_h.data.sublist(compressed_pixels, 3)
|
113
115
|
|
@@ -325,41 +327,49 @@ class ImageDraw:
|
|
325
327
|
color_zone_clean: Color,
|
326
328
|
color_no_go: Color,
|
327
329
|
) -> NumpyArray:
|
328
|
-
"""Get the zone clean from the JSON data."""
|
330
|
+
"""Get the zone clean from the JSON data with parallel processing."""
|
331
|
+
|
329
332
|
try:
|
330
333
|
zone_clean = self.img_h.data.find_zone_entities(m_json)
|
331
334
|
except (ValueError, KeyError):
|
332
335
|
zone_clean = None
|
333
336
|
else:
|
334
337
|
_LOGGER.info("%s: Got zones.", self.file_name)
|
338
|
+
|
335
339
|
if zone_clean:
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
+
# Prepare zone drawing tasks for parallel execution
|
341
|
+
zone_tasks = []
|
342
|
+
|
343
|
+
# Active zones
|
344
|
+
zones_active = zone_clean.get("active_zone")
|
340
345
|
if zones_active:
|
341
|
-
|
342
|
-
np_array, zones_active, color_zone_clean
|
346
|
+
zone_tasks.append(
|
347
|
+
self.img_h.draw.zones(np_array.copy(), zones_active, color_zone_clean)
|
343
348
|
)
|
344
|
-
try:
|
345
|
-
no_go_zones = zone_clean.get("no_go_area")
|
346
|
-
except KeyError:
|
347
|
-
no_go_zones = None
|
348
349
|
|
350
|
+
# No-go zones
|
351
|
+
no_go_zones = zone_clean.get("no_go_area")
|
349
352
|
if no_go_zones:
|
350
|
-
|
351
|
-
np_array, no_go_zones, color_no_go
|
353
|
+
zone_tasks.append(
|
354
|
+
self.img_h.draw.zones(np_array.copy(), no_go_zones, color_no_go)
|
352
355
|
)
|
353
356
|
|
354
|
-
|
355
|
-
|
356
|
-
except KeyError:
|
357
|
-
no_mop_zones = None
|
358
|
-
|
357
|
+
# No-mop zones
|
358
|
+
no_mop_zones = zone_clean.get("no_mop_area")
|
359
359
|
if no_mop_zones:
|
360
|
-
|
361
|
-
np_array, no_mop_zones, color_no_go
|
360
|
+
zone_tasks.append(
|
361
|
+
self.img_h.draw.zones(np_array.copy(), no_mop_zones, color_no_go)
|
362
362
|
)
|
363
|
+
|
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
|
+
|
363
373
|
return np_array
|
364
374
|
|
365
375
|
async def async_draw_virtual_walls(
|
@@ -429,7 +439,6 @@ class ImageDraw:
|
|
429
439
|
def _check_active_zone_and_set_zooming(self) -> None:
|
430
440
|
"""Helper function to check active zones and set zooming state."""
|
431
441
|
if self.img_h.active_zones and self.img_h.robot_in_room:
|
432
|
-
from .config.types import RoomStore
|
433
442
|
|
434
443
|
segment_id = str(self.img_h.robot_in_room["id"])
|
435
444
|
room_store = RoomStore(self.file_name)
|
@@ -606,7 +615,6 @@ class ImageDraw:
|
|
606
615
|
|
607
616
|
# Handle active zones - Map segment ID to active_zones position
|
608
617
|
if self.img_h.active_zones:
|
609
|
-
from .config.types import RoomStore
|
610
618
|
|
611
619
|
segment_id = str(self.img_h.robot_in_room["id"])
|
612
620
|
room_store = RoomStore(self.file_name)
|
@@ -7,13 +7,16 @@ Version: 0.1.9
|
|
7
7
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
|
+
import asyncio
|
10
11
|
import json
|
11
12
|
|
12
13
|
from PIL import Image
|
13
14
|
|
15
|
+
from .config.async_utils import AsyncNumPy, AsyncPIL, AsyncParallel
|
14
16
|
from .config.auto_crop import AutoCrop
|
15
17
|
from .config.drawable_elements import DrawableElement
|
16
18
|
from .config.shared import CameraShared
|
19
|
+
from .config.utils import pil_to_webp_bytes
|
17
20
|
from .config.types import (
|
18
21
|
COLORS,
|
19
22
|
LOGGER,
|
@@ -291,14 +294,34 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
291
294
|
)
|
292
295
|
# Copy the base layer to the new image.
|
293
296
|
img_np_array = await self.async_copy_array(self.img_base_layer)
|
294
|
-
|
295
|
-
#
|
297
|
+
|
298
|
+
# Prepare parallel data extraction tasks
|
299
|
+
data_tasks = []
|
300
|
+
|
301
|
+
# Prepare zone data extraction
|
302
|
+
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
303
|
+
data_tasks.append(self._prepare_zone_data(m_json))
|
304
|
+
|
305
|
+
# Prepare go_to flag data extraction
|
306
|
+
if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
|
307
|
+
data_tasks.append(self._prepare_goto_data(entity_dict))
|
308
|
+
|
309
|
+
# Prepare path data extraction
|
310
|
+
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
311
|
+
LOGGER.info("%s: PATH element enabled: %s", self.file_name, path_enabled)
|
312
|
+
if path_enabled:
|
313
|
+
LOGGER.info("%s: Drawing path", self.file_name)
|
314
|
+
data_tasks.append(self._prepare_path_data(m_json))
|
315
|
+
|
316
|
+
# Execute data preparation in parallel if we have tasks
|
317
|
+
if data_tasks:
|
318
|
+
prepared_data = await AsyncParallel.parallel_data_preparation(*data_tasks)
|
319
|
+
|
320
|
+
# Process drawing operations sequentially (since they modify the same array)
|
321
|
+
# Draw zones if enabled
|
296
322
|
if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
|
297
323
|
img_np_array = await self.imd.async_draw_zones(
|
298
|
-
m_json,
|
299
|
-
img_np_array,
|
300
|
-
colors["zone_clean"],
|
301
|
-
colors["no_go"],
|
324
|
+
m_json, img_np_array, colors["zone_clean"], colors["no_go"]
|
302
325
|
)
|
303
326
|
|
304
327
|
# Draw the go_to target flag if enabled
|
@@ -307,13 +330,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
307
330
|
img_np_array, entity_dict, colors["go_to"]
|
308
331
|
)
|
309
332
|
|
310
|
-
# Draw
|
311
|
-
path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
|
312
|
-
LOGGER.info(
|
313
|
-
"%s: PATH element enabled: %s", self.file_name, path_enabled
|
314
|
-
)
|
333
|
+
# Draw paths if enabled
|
315
334
|
if path_enabled:
|
316
|
-
LOGGER.info("%s: Drawing path", self.file_name)
|
317
335
|
img_np_array = await self.imd.async_draw_paths(
|
318
336
|
img_np_array, m_json, colors["move"], self.color_grey
|
319
337
|
)
|
@@ -371,15 +389,13 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
371
389
|
# Handle resizing if needed, then return based on format preference
|
372
390
|
if self.check_zoom_and_aspect_ratio():
|
373
391
|
# Convert to PIL for resizing
|
374
|
-
pil_img =
|
392
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
375
393
|
del img_np_array
|
376
394
|
resize_params = prepare_resize_params(self, pil_img, False)
|
377
395
|
resized_image = await self.async_resize_images(resize_params)
|
378
396
|
|
379
397
|
# Return WebP bytes or PIL Image based on parameter
|
380
398
|
if return_webp:
|
381
|
-
from .config.utils import pil_to_webp_bytes
|
382
|
-
|
383
399
|
webp_bytes = await pil_to_webp_bytes(resized_image)
|
384
400
|
return webp_bytes
|
385
401
|
else:
|
@@ -394,7 +410,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
394
410
|
return webp_bytes
|
395
411
|
else:
|
396
412
|
# Convert to PIL Image (original behavior)
|
397
|
-
pil_img =
|
413
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
398
414
|
del img_np_array
|
399
415
|
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
400
416
|
return pil_img
|
@@ -474,4 +490,26 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
474
490
|
@staticmethod
|
475
491
|
async def async_copy_array(original_array):
|
476
492
|
"""Copy the array."""
|
477
|
-
return
|
493
|
+
return await AsyncNumPy.async_copy(original_array)
|
494
|
+
|
495
|
+
async def _prepare_zone_data(self, m_json):
|
496
|
+
"""Prepare zone data for parallel processing."""
|
497
|
+
await asyncio.sleep(0) # Yield control
|
498
|
+
try:
|
499
|
+
return self.data.find_zone_entities(m_json)
|
500
|
+
except (ValueError, KeyError):
|
501
|
+
return None
|
502
|
+
|
503
|
+
async def _prepare_goto_data(self, entity_dict):
|
504
|
+
"""Prepare go-to flag data for parallel processing."""
|
505
|
+
await asyncio.sleep(0) # Yield control
|
506
|
+
# Extract go-to target data from entity_dict
|
507
|
+
return entity_dict.get("go_to_target", None)
|
508
|
+
|
509
|
+
async def _prepare_path_data(self, m_json):
|
510
|
+
"""Prepare path data for parallel processing."""
|
511
|
+
await asyncio.sleep(0) # Yield control
|
512
|
+
try:
|
513
|
+
return self.data.find_path_entities(m_json)
|
514
|
+
except (ValueError, KeyError):
|
515
|
+
return None
|
{valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/map_data.py
RENAMED
@@ -9,7 +9,7 @@ Version: v0.1.6
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
11
|
import numpy as np
|
12
|
-
|
12
|
+
import pandas as pd
|
13
13
|
from .config.types import ImageSize, JsonType
|
14
14
|
|
15
15
|
|
@@ -18,65 +18,48 @@ class ImageData:
|
|
18
18
|
|
19
19
|
@staticmethod
|
20
20
|
def sublist(lst, n):
|
21
|
-
"""
|
22
|
-
return [lst[i
|
21
|
+
"""Split a list into n chunks of specified size."""
|
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
|
-
"""Join the lists in a unique list of n elements"""
|
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
|
29
|
+
result = [arr[i: i + n].tolist() for i in range(num_windows)]
|
30
30
|
return result
|
31
31
|
|
32
|
-
# The below functions are basically the same ech one
|
33
|
-
# of them is allowing filtering and putting together in a
|
34
|
-
# list the specific Layers, Paths, Zones and Pints in the
|
35
|
-
# Vacuums Json in parallel.
|
36
|
-
|
37
32
|
@staticmethod
|
38
33
|
def get_obstacles(entity_dict: dict) -> list:
|
39
34
|
"""Get the obstacles positions from the entity data."""
|
40
|
-
|
41
|
-
obstacle_data = entity_dict.get("obstacle")
|
42
|
-
except KeyError:
|
43
|
-
return []
|
35
|
+
obstacles = entity_dict.get("obstacle", [])
|
44
36
|
obstacle_positions = []
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
return []
|
60
|
-
|
61
|
-
@staticmethod
|
62
|
-
def find_layers(
|
63
|
-
json_obj: JsonType, layer_dict: dict, active_list: list
|
64
|
-
) -> tuple[dict, list]:
|
37
|
+
for obstacle in obstacles:
|
38
|
+
label = obstacle.get("metaData", {}).get("label")
|
39
|
+
points = obstacle.get("points", [])
|
40
|
+
image_id = obstacle.get("metaData", {}).get("id")
|
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
|
+
})
|
47
|
+
return obstacle_positions
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def find_layers(json_obj: JsonType, layer_dict: dict, active_list: list) -> tuple[dict, list]:
|
65
51
|
"""Find the layers in the json object."""
|
66
52
|
layer_dict = {} if layer_dict is None else layer_dict
|
67
53
|
active_list = [] if active_list is None else active_list
|
68
54
|
if isinstance(json_obj, dict):
|
69
|
-
if
|
55
|
+
if json_obj.get("__class") == "MapLayer":
|
70
56
|
layer_type = json_obj.get("type")
|
71
57
|
active_type = json_obj.get("metaData")
|
72
58
|
if layer_type:
|
73
|
-
|
74
|
-
layer_dict[layer_type] = []
|
75
|
-
layer_dict[layer_type].append(json_obj.get("compressedPixels", []))
|
59
|
+
layer_dict.setdefault(layer_type, []).append(json_obj.get("compressedPixels", []))
|
76
60
|
if layer_type == "segment":
|
77
|
-
active_list.append(int(active_type
|
78
|
-
|
79
|
-
for value in json_obj.items():
|
61
|
+
active_list.append(int(active_type.get("active", 0)))
|
62
|
+
for value in json_obj.values():
|
80
63
|
ImageData.find_layers(value, layer_dict, active_list)
|
81
64
|
elif isinstance(json_obj, list):
|
82
65
|
for item in json_obj:
|
@@ -86,8 +69,7 @@ class ImageData:
|
|
86
69
|
@staticmethod
|
87
70
|
def find_points_entities(json_obj: JsonType, entity_dict: dict = None) -> dict:
|
88
71
|
"""Find the points entities in the json object."""
|
89
|
-
if entity_dict is None
|
90
|
-
entity_dict = {}
|
72
|
+
entity_dict = {} if entity_dict is None else entity_dict
|
91
73
|
if isinstance(json_obj, dict):
|
92
74
|
if json_obj.get("__class") == "PointMapEntity":
|
93
75
|
entity_type = json_obj.get("type")
|
@@ -103,9 +85,7 @@ class ImageData:
|
|
103
85
|
@staticmethod
|
104
86
|
def find_paths_entities(json_obj: JsonType, entity_dict: dict = None) -> dict:
|
105
87
|
"""Find the paths entities in the json object."""
|
106
|
-
|
107
|
-
if entity_dict is None:
|
108
|
-
entity_dict = {}
|
88
|
+
entity_dict = {} if entity_dict is None else entity_dict
|
109
89
|
if isinstance(json_obj, dict):
|
110
90
|
if json_obj.get("__class") == "PathMapEntity":
|
111
91
|
entity_type = json_obj.get("type")
|
@@ -121,8 +101,7 @@ class ImageData:
|
|
121
101
|
@staticmethod
|
122
102
|
def find_zone_entities(json_obj: JsonType, entity_dict: dict = None) -> dict:
|
123
103
|
"""Find the zone entities in the json object."""
|
124
|
-
if entity_dict is None
|
125
|
-
entity_dict = {}
|
104
|
+
entity_dict = {} if entity_dict is None else entity_dict
|
126
105
|
if isinstance(json_obj, dict):
|
127
106
|
if json_obj.get("__class") == "PolygonMapEntity":
|
128
107
|
entity_type = json_obj.get("type")
|
@@ -138,59 +117,41 @@ class ImageData:
|
|
138
117
|
@staticmethod
|
139
118
|
def find_virtual_walls(json_obj: JsonType) -> list:
|
140
119
|
"""Find the virtual walls in the json object."""
|
141
|
-
|
120
|
+
walls = []
|
142
121
|
|
143
|
-
def
|
144
|
-
"""Find the virtual walls in the json object recursively."""
|
122
|
+
def _recursive(obj):
|
145
123
|
if isinstance(obj, dict):
|
146
|
-
if obj.get("__class") == "LineMapEntity":
|
147
|
-
|
148
|
-
if entity_type == "virtual_wall":
|
149
|
-
virtual_walls.append(obj["points"])
|
124
|
+
if obj.get("__class") == "LineMapEntity" and obj.get("type") == "virtual_wall":
|
125
|
+
walls.append(obj["points"])
|
150
126
|
for value in obj.values():
|
151
|
-
|
127
|
+
_recursive(value)
|
152
128
|
elif isinstance(obj, list):
|
153
129
|
for item in obj:
|
154
|
-
|
130
|
+
_recursive(item)
|
155
131
|
|
156
|
-
|
157
|
-
return
|
132
|
+
_recursive(json_obj)
|
133
|
+
return walls
|
158
134
|
|
159
135
|
@staticmethod
|
160
|
-
async def async_get_rooms_coordinates(
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
for entry in pixels:
|
174
|
-
if rand:
|
175
|
-
x, y, _ = entry # Extract x and y coordinates
|
176
|
-
max_x = max(max_x, x) # Update max x coordinate
|
177
|
-
max_y = max(max_y, y + pixel_size) # Update max y coordinate
|
178
|
-
min_x = min(min_x, x) # Update min x coordinate
|
179
|
-
min_y = min(min_y, y) # Update min y coordinate
|
180
|
-
else:
|
181
|
-
x, y, z = entry # Extract x and y coordinates
|
182
|
-
max_x = max(max_x, x + z) # Update max x coordinate
|
183
|
-
max_y = max(max_y, y + pixel_size) # Update max y coordinate
|
184
|
-
min_x = min(min_x, x) # Update min x coordinate
|
185
|
-
min_y = min(min_y, y) # Update min y coordinate
|
136
|
+
async def async_get_rooms_coordinates(pixels: list, pixel_size: int = 5, rand: bool = False) -> tuple:
|
137
|
+
"""Extract the room coordinates from the vacuum pixels data."""
|
138
|
+
df = pd.DataFrame(pixels, columns=["x", "y", "length"])
|
139
|
+
if rand:
|
140
|
+
df["x_end"] = df["x"]
|
141
|
+
df["y_end"] = df["y"] + pixel_size
|
142
|
+
else:
|
143
|
+
df["x_end"] = df["x"] + df["length"]
|
144
|
+
df["y_end"] = df["y"] + pixel_size
|
145
|
+
|
146
|
+
min_x, max_x = df["x"].min(), df["x_end"].max()
|
147
|
+
min_y, max_y = df["y"].min(), df["y_end"].max()
|
148
|
+
|
186
149
|
if rand:
|
187
150
|
return (
|
188
|
-
((
|
189
|
-
(
|
190
|
-
((min_x * pixel_size) * 10),
|
191
|
-
((min_y * pixel_size) * 10),
|
192
|
-
),
|
151
|
+
((max_x * pixel_size) * 10, (max_y * pixel_size) * 10),
|
152
|
+
((min_x * pixel_size) * 10, (min_y * pixel_size) * 10),
|
193
153
|
)
|
154
|
+
|
194
155
|
return (
|
195
156
|
min_x * pixel_size,
|
196
157
|
min_y * pixel_size,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "valetudo-map-parser"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.9b62"
|
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"
|
@@ -18,6 +18,7 @@ python = ">=3.12"
|
|
18
18
|
numpy = ">=1.26.4"
|
19
19
|
Pillow = ">=10.3.0"
|
20
20
|
scipy = ">=1.12.0"
|
21
|
+
pandas = ">=2.3.0"
|
21
22
|
|
22
23
|
[tool.poetry.group.dev.dependencies]
|
23
24
|
ruff = "*"
|
File without changes
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/py.typed
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b60 → valetudo_map_parser-0.1.9b62}/SCR/valetudo_map_parser/reimg_draw.py
RENAMED
File without changes
|
File without changes
|