valetudo-map-parser 0.1.9b59__tar.gz → 0.1.9b61__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.9b59 → valetudo_map_parser-0.1.9b61}/PKG-INFO +2 -1
- valetudo_map_parser-0.1.9b61/SCR/valetudo_map_parser/config/async_utils.py +55 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/drawable.py +2 -1
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/hypfer_handler.py +4 -3
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/map_data.py +62 -92
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/pyproject.toml +2 -1
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/README.md +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/colors.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/rand256_parser.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/shared.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/types.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/config/utils.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/hypfer_draw.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/rand256_handler.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
- {valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/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.9b61
|
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,55 @@
|
|
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 PIL import Image
|
9
|
+
|
10
|
+
|
11
|
+
async def make_async(func: Callable, *args, **kwargs) -> Any:
|
12
|
+
"""Convert a synchronous function to async by yielding control to the event loop."""
|
13
|
+
result = func(*args, **kwargs)
|
14
|
+
await asyncio.sleep(0)
|
15
|
+
return result
|
16
|
+
|
17
|
+
|
18
|
+
class AsyncNumPy:
|
19
|
+
"""Async wrappers for NumPy operations that yield control to the event loop."""
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
async def async_copy(array: np.ndarray) -> np.ndarray:
|
23
|
+
"""Async array copying."""
|
24
|
+
return await make_async(np.copy, array)
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
async def async_full(shape: tuple, fill_value: Any, dtype: np.dtype = None) -> np.ndarray:
|
28
|
+
"""Async array creation with fill value."""
|
29
|
+
return await make_async(np.full, shape, fill_value, dtype=dtype)
|
30
|
+
|
31
|
+
|
32
|
+
class AsyncPIL:
|
33
|
+
"""Async wrappers for PIL operations that yield control to the event loop."""
|
34
|
+
|
35
|
+
@staticmethod
|
36
|
+
async def async_fromarray(array: np.ndarray, mode: str = "RGBA") -> Image.Image:
|
37
|
+
"""Async PIL Image creation from NumPy array."""
|
38
|
+
return await make_async(Image.fromarray, array, mode)
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
async def async_resize(image: Image.Image, size: tuple, resample: int = None) -> Image.Image:
|
42
|
+
"""Async image resizing."""
|
43
|
+
if resample is None:
|
44
|
+
resample = Image.LANCZOS
|
45
|
+
return await make_async(image.resize, size, resample)
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
async def async_save_to_bytes(image: Image.Image, format_type: str = "WEBP", **kwargs) -> bytes:
|
49
|
+
"""Async image saving to bytes."""
|
50
|
+
def save_to_bytes():
|
51
|
+
buffer = io.BytesIO()
|
52
|
+
image.save(buffer, format=format_type, **kwargs)
|
53
|
+
return buffer.getvalue()
|
54
|
+
|
55
|
+
return await make_async(save_to_bytes)
|
@@ -16,6 +16,7 @@ import math
|
|
16
16
|
import numpy as np
|
17
17
|
from PIL import ImageDraw, ImageFont
|
18
18
|
|
19
|
+
from .async_utils import AsyncNumPy
|
19
20
|
from .color_utils import get_blended_color
|
20
21
|
from .colors import ColorsManagement
|
21
22
|
from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
|
@@ -45,7 +46,7 @@ class Drawable:
|
|
45
46
|
) -> NumpyArray:
|
46
47
|
"""Create the empty background image NumPy array.
|
47
48
|
Background color is specified as an RGBA tuple."""
|
48
|
-
return
|
49
|
+
return await AsyncNumPy.async_full((height, width, 4), background_color, dtype=np.uint8)
|
49
50
|
|
50
51
|
@staticmethod
|
51
52
|
async def from_json_to_image(
|
@@ -11,6 +11,7 @@ import json
|
|
11
11
|
|
12
12
|
from PIL import Image
|
13
13
|
|
14
|
+
from .config.async_utils import AsyncNumPy, AsyncPIL
|
14
15
|
from .config.auto_crop import AutoCrop
|
15
16
|
from .config.drawable_elements import DrawableElement
|
16
17
|
from .config.shared import CameraShared
|
@@ -371,7 +372,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
371
372
|
# Handle resizing if needed, then return based on format preference
|
372
373
|
if self.check_zoom_and_aspect_ratio():
|
373
374
|
# Convert to PIL for resizing
|
374
|
-
pil_img =
|
375
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
375
376
|
del img_np_array
|
376
377
|
resize_params = prepare_resize_params(self, pil_img, False)
|
377
378
|
resized_image = await self.async_resize_images(resize_params)
|
@@ -394,7 +395,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
394
395
|
return webp_bytes
|
395
396
|
else:
|
396
397
|
# Convert to PIL Image (original behavior)
|
397
|
-
pil_img =
|
398
|
+
pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
|
398
399
|
del img_np_array
|
399
400
|
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
400
401
|
return pil_img
|
@@ -474,4 +475,4 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
474
475
|
@staticmethod
|
475
476
|
async def async_copy_array(original_array):
|
476
477
|
"""Copy the array."""
|
477
|
-
return
|
478
|
+
return await AsyncNumPy.async_copy(original_array)
|
{valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/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,
|
@@ -308,8 +269,17 @@ class RandImageData:
|
|
308
269
|
Return the calculated angle and original angle.
|
309
270
|
"""
|
310
271
|
angle_c = round(json_data.get("robot_angle", 0))
|
311
|
-
|
312
|
-
|
272
|
+
# Convert negative values: -10 -> 350, -180 -> 359, but keep positive: 24 -> 24
|
273
|
+
if angle_c < 0:
|
274
|
+
if angle_c == -180:
|
275
|
+
angle = 359 # -180 becomes 359 (avoiding 360)
|
276
|
+
else:
|
277
|
+
angle = 360 + angle_c # -10 -> 350, -90 -> 270
|
278
|
+
else:
|
279
|
+
angle = angle_c
|
280
|
+
|
281
|
+
angle = (angle + 90) % 360
|
282
|
+
return angle, json_data.get("robot_angle", 0)
|
313
283
|
|
314
284
|
@staticmethod
|
315
285
|
def get_rrm_goto_target(json_data: JsonType) -> list or None:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "valetudo-map-parser"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.9b61"
|
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.9b59 → valetudo_map_parser-0.1.9b61}/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
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/hypfer_draw.py
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/py.typed
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b59 → valetudo_map_parser-0.1.9b61}/SCR/valetudo_map_parser/reimg_draw.py
RENAMED
File without changes
|
File without changes
|