valetudo-map-parser 0.1.9b57__tar.gz → 0.1.9b59__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.9b57 → valetudo_map_parser-0.1.9b59}/PKG-INFO +1 -1
- valetudo_map_parser-0.1.9b59/SCR/valetudo_map_parser/config/rand256_parser.py +395 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/utils.py +20 -18
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/hypfer_handler.py +3 -10
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/rand256_handler.py +1 -5
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/pyproject.toml +1 -1
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/README.md +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/colors.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/drawable.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/shared.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/config/types.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/hypfer_draw.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/map_data.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
- {valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/rooms_handler.py +0 -0
@@ -0,0 +1,395 @@
|
|
1
|
+
"""New Rand256 Map Parser - Based on Xiaomi/Roborock implementation with precise binary parsing."""
|
2
|
+
|
3
|
+
import struct
|
4
|
+
import math
|
5
|
+
from enum import Enum
|
6
|
+
from typing import Any, Dict, List, Optional
|
7
|
+
|
8
|
+
|
9
|
+
class RRMapParser:
|
10
|
+
"""New Rand256 Map Parser using Xiaomi/Roborock approach for precise data extraction."""
|
11
|
+
|
12
|
+
class Types(Enum):
|
13
|
+
"""Map data block types."""
|
14
|
+
|
15
|
+
CHARGER_LOCATION = 1
|
16
|
+
IMAGE = 2
|
17
|
+
PATH = 3
|
18
|
+
GOTO_PATH = 4
|
19
|
+
GOTO_PREDICTED_PATH = 5
|
20
|
+
CURRENTLY_CLEANED_ZONES = 6
|
21
|
+
GOTO_TARGET = 7
|
22
|
+
ROBOT_POSITION = 8
|
23
|
+
FORBIDDEN_ZONES = 9
|
24
|
+
VIRTUAL_WALLS = 10
|
25
|
+
CURRENTLY_CLEANED_BLOCKS = 11
|
26
|
+
FORBIDDEN_MOP_ZONES = 12
|
27
|
+
|
28
|
+
class Tools:
|
29
|
+
"""Tools for coordinate transformations."""
|
30
|
+
|
31
|
+
DIMENSION_PIXELS = 1024
|
32
|
+
DIMENSION_MM = 50 * 1024
|
33
|
+
|
34
|
+
def __init__(self):
|
35
|
+
"""Initialize the parser."""
|
36
|
+
self.map_data: Dict[str, Any] = {}
|
37
|
+
|
38
|
+
# Xiaomi/Roborock style byte extraction methods
|
39
|
+
@staticmethod
|
40
|
+
def _get_bytes(data: bytes, start_index: int, size: int) -> bytes:
|
41
|
+
"""Extract bytes from data."""
|
42
|
+
return data[start_index : start_index + size]
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
def _get_int8(data: bytes, address: int) -> int:
|
46
|
+
"""Get an 8-bit integer from data using Xiaomi method."""
|
47
|
+
return data[address] & 0xFF
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def _get_int16(data: bytes, address: int) -> int:
|
51
|
+
"""Get a 16-bit little-endian integer using Xiaomi method."""
|
52
|
+
return ((data[address + 0] << 0) & 0xFF) | ((data[address + 1] << 8) & 0xFFFF)
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def _get_int32(data: bytes, address: int) -> int:
|
56
|
+
"""Get a 32-bit little-endian integer using Xiaomi method."""
|
57
|
+
return (
|
58
|
+
((data[address + 0] << 0) & 0xFF)
|
59
|
+
| ((data[address + 1] << 8) & 0xFFFF)
|
60
|
+
| ((data[address + 2] << 16) & 0xFFFFFF)
|
61
|
+
| ((data[address + 3] << 24) & 0xFFFFFFFF)
|
62
|
+
)
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
def _get_int32_signed(data: bytes, address: int) -> int:
|
66
|
+
"""Get a 32-bit signed integer."""
|
67
|
+
value = RRMapParser._get_int32(data, address)
|
68
|
+
return value if value < 0x80000000 else value - 0x100000000
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
def _parse_object_position(block_data_length: int, data: bytes) -> Dict[str, Any]:
|
72
|
+
"""Parse object position using Xiaomi method."""
|
73
|
+
x = RRMapParser._get_int32(data, 0x00)
|
74
|
+
y = RRMapParser._get_int32(data, 0x04)
|
75
|
+
angle = 0
|
76
|
+
if block_data_length > 8:
|
77
|
+
raw_angle = RRMapParser._get_int32(data, 0x08)
|
78
|
+
# Apply Xiaomi angle normalization
|
79
|
+
if raw_angle > 0xFF:
|
80
|
+
angle = (raw_angle & 0xFF) - 256
|
81
|
+
else:
|
82
|
+
angle = raw_angle
|
83
|
+
return {"position": [x, y], "angle": angle}
|
84
|
+
|
85
|
+
@staticmethod
|
86
|
+
def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
|
87
|
+
"""Parse path block using EXACT same method as working parser."""
|
88
|
+
points = [
|
89
|
+
[
|
90
|
+
struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
|
91
|
+
struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
|
92
|
+
]
|
93
|
+
for i in range(0, length, 4)
|
94
|
+
]
|
95
|
+
return {
|
96
|
+
"current_angle": struct.unpack("<I", buf[offset + 16 : offset + 20])[0],
|
97
|
+
"points": points,
|
98
|
+
}
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _parse_goto_target(data: bytes) -> List[int]:
|
102
|
+
"""Parse goto target using Xiaomi method."""
|
103
|
+
try:
|
104
|
+
x = RRMapParser._get_int16(data, 0x00)
|
105
|
+
y = RRMapParser._get_int16(data, 0x02)
|
106
|
+
return [x, y]
|
107
|
+
except (struct.error, IndexError):
|
108
|
+
return [0, 0]
|
109
|
+
|
110
|
+
def parse(self, map_buf: bytes) -> Dict[str, Any]:
|
111
|
+
"""Parse the map header data using Xiaomi method."""
|
112
|
+
if len(map_buf) < 18 or map_buf[0:2] != b"rr":
|
113
|
+
return {}
|
114
|
+
|
115
|
+
try:
|
116
|
+
return {
|
117
|
+
"header_length": self._get_int16(map_buf, 0x02),
|
118
|
+
"data_length": self._get_int16(map_buf, 0x04),
|
119
|
+
"version": {
|
120
|
+
"major": self._get_int16(map_buf, 0x08),
|
121
|
+
"minor": self._get_int16(map_buf, 0x0A),
|
122
|
+
},
|
123
|
+
"map_index": self._get_int32(map_buf, 0x0C),
|
124
|
+
"map_sequence": self._get_int32(map_buf, 0x10),
|
125
|
+
}
|
126
|
+
except (struct.error, IndexError):
|
127
|
+
return {}
|
128
|
+
|
129
|
+
def parse_blocks(self, raw: bytes, pixels: bool = True) -> Dict[int, Any]:
|
130
|
+
"""Parse all blocks using Xiaomi method."""
|
131
|
+
blocks = {}
|
132
|
+
map_header_length = self._get_int16(raw, 0x02)
|
133
|
+
block_start_position = map_header_length
|
134
|
+
|
135
|
+
while block_start_position < len(raw):
|
136
|
+
try:
|
137
|
+
# Parse block header using Xiaomi method
|
138
|
+
block_header_length = self._get_int16(raw, block_start_position + 0x02)
|
139
|
+
header = self._get_bytes(raw, block_start_position, block_header_length)
|
140
|
+
block_type = self._get_int16(header, 0x00)
|
141
|
+
block_data_length = self._get_int32(header, 0x04)
|
142
|
+
block_data_start = block_start_position + block_header_length
|
143
|
+
data = self._get_bytes(raw, block_data_start, block_data_length)
|
144
|
+
|
145
|
+
# Parse different block types
|
146
|
+
if block_type == self.Types.ROBOT_POSITION.value:
|
147
|
+
blocks[block_type] = self._parse_object_position(
|
148
|
+
block_data_length, data
|
149
|
+
)
|
150
|
+
elif block_type == self.Types.CHARGER_LOCATION.value:
|
151
|
+
blocks[block_type] = self._parse_object_position(
|
152
|
+
block_data_length, data
|
153
|
+
)
|
154
|
+
elif block_type == self.Types.PATH.value:
|
155
|
+
blocks[block_type] = self._parse_path_block(
|
156
|
+
raw, block_start_position, block_data_length
|
157
|
+
)
|
158
|
+
elif block_type == self.Types.GOTO_PREDICTED_PATH.value:
|
159
|
+
blocks[block_type] = self._parse_path_block(
|
160
|
+
raw, block_start_position, block_data_length
|
161
|
+
)
|
162
|
+
elif block_type == self.Types.GOTO_TARGET.value:
|
163
|
+
blocks[block_type] = {"position": self._parse_goto_target(data)}
|
164
|
+
elif block_type == self.Types.IMAGE.value:
|
165
|
+
# Get header length for Gen1/Gen3 detection
|
166
|
+
header_length = self._get_int8(header, 2)
|
167
|
+
blocks[block_type] = self._parse_image_block(
|
168
|
+
raw,
|
169
|
+
block_start_position,
|
170
|
+
block_data_length,
|
171
|
+
header_length,
|
172
|
+
pixels,
|
173
|
+
)
|
174
|
+
|
175
|
+
# Move to next block using Xiaomi method
|
176
|
+
block_start_position = (
|
177
|
+
block_start_position + block_data_length + self._get_int8(header, 2)
|
178
|
+
)
|
179
|
+
|
180
|
+
except (struct.error, IndexError):
|
181
|
+
break
|
182
|
+
|
183
|
+
return blocks
|
184
|
+
|
185
|
+
def _parse_image_block(
|
186
|
+
self, buf: bytes, offset: int, length: int, hlength: int, pixels: bool = True
|
187
|
+
) -> Dict[str, Any]:
|
188
|
+
"""Parse image block using EXACT logic from working parser."""
|
189
|
+
try:
|
190
|
+
# CRITICAL: Gen1 vs Gen3 detection like working parser
|
191
|
+
g3offset = 4 if hlength > 24 else 0
|
192
|
+
|
193
|
+
# Use EXACT same structure as working parser
|
194
|
+
parameters = {
|
195
|
+
"segments": {
|
196
|
+
"count": (
|
197
|
+
struct.unpack("<i", buf[offset + 8 : offset + 12])[0]
|
198
|
+
if g3offset
|
199
|
+
else 0
|
200
|
+
),
|
201
|
+
"id": [],
|
202
|
+
},
|
203
|
+
"position": {
|
204
|
+
"top": struct.unpack(
|
205
|
+
"<i", buf[offset + 8 + g3offset : offset + 12 + g3offset]
|
206
|
+
)[0],
|
207
|
+
"left": struct.unpack(
|
208
|
+
"<i", buf[offset + 12 + g3offset : offset + 16 + g3offset]
|
209
|
+
)[0],
|
210
|
+
},
|
211
|
+
"dimensions": {
|
212
|
+
"height": struct.unpack(
|
213
|
+
"<i", buf[offset + 16 + g3offset : offset + 20 + g3offset]
|
214
|
+
)[0],
|
215
|
+
"width": struct.unpack(
|
216
|
+
"<i", buf[offset + 20 + g3offset : offset + 24 + g3offset]
|
217
|
+
)[0],
|
218
|
+
},
|
219
|
+
"pixels": {"floor": [], "walls": [], "segments": {}},
|
220
|
+
}
|
221
|
+
|
222
|
+
# Apply EXACT working parser coordinate transformation
|
223
|
+
parameters["position"]["top"] = (
|
224
|
+
self.Tools.DIMENSION_PIXELS
|
225
|
+
- parameters["position"]["top"]
|
226
|
+
- parameters["dimensions"]["height"]
|
227
|
+
)
|
228
|
+
|
229
|
+
# Extract pixels using optimized sequential processing
|
230
|
+
if (
|
231
|
+
parameters["dimensions"]["height"] > 0
|
232
|
+
and parameters["dimensions"]["width"] > 0
|
233
|
+
):
|
234
|
+
# Process data sequentially - segments are organized as blocks
|
235
|
+
current_segments = {}
|
236
|
+
|
237
|
+
for i in range(length):
|
238
|
+
pixel_byte = struct.unpack(
|
239
|
+
"<B",
|
240
|
+
buf[offset + 24 + g3offset + i : offset + 25 + g3offset + i],
|
241
|
+
)[0]
|
242
|
+
|
243
|
+
segment_type = pixel_byte & 0x07
|
244
|
+
if segment_type == 0:
|
245
|
+
continue
|
246
|
+
|
247
|
+
if segment_type == 1 and pixels:
|
248
|
+
# Wall pixel
|
249
|
+
parameters["pixels"]["walls"].append(i)
|
250
|
+
else:
|
251
|
+
# Floor or room segment
|
252
|
+
segment_id = pixel_byte >> 3
|
253
|
+
if segment_id == 0 and pixels:
|
254
|
+
# Floor pixel
|
255
|
+
parameters["pixels"]["floor"].append(i)
|
256
|
+
elif segment_id != 0:
|
257
|
+
# Room segment - segments are sequential blocks
|
258
|
+
if segment_id not in current_segments:
|
259
|
+
parameters["segments"]["id"].append(segment_id)
|
260
|
+
parameters["segments"][
|
261
|
+
"pixels_seg_" + str(segment_id)
|
262
|
+
] = []
|
263
|
+
current_segments[segment_id] = True
|
264
|
+
|
265
|
+
if pixels:
|
266
|
+
parameters["segments"][
|
267
|
+
"pixels_seg_" + str(segment_id)
|
268
|
+
].append(i)
|
269
|
+
|
270
|
+
parameters["segments"]["count"] = len(parameters["segments"]["id"])
|
271
|
+
return parameters
|
272
|
+
|
273
|
+
except (struct.error, IndexError):
|
274
|
+
return {
|
275
|
+
"segments": {"count": 0, "id": []},
|
276
|
+
"position": {"top": 0, "left": 0},
|
277
|
+
"dimensions": {"height": 0, "width": 0},
|
278
|
+
"pixels": {"floor": [], "walls": [], "segments": {}},
|
279
|
+
}
|
280
|
+
|
281
|
+
def parse_rrm_data(
|
282
|
+
self, map_buf: bytes, pixels: bool = False
|
283
|
+
) -> Optional[Dict[str, Any]]:
|
284
|
+
"""Parse the complete map data and return in your JSON format."""
|
285
|
+
if not self.parse(map_buf).get("map_index"):
|
286
|
+
return None
|
287
|
+
|
288
|
+
try:
|
289
|
+
parsed_map_data = {}
|
290
|
+
blocks = self.parse_blocks(map_buf, pixels)
|
291
|
+
|
292
|
+
# Parse robot position
|
293
|
+
if self.Types.ROBOT_POSITION.value in blocks:
|
294
|
+
robot_data = blocks[self.Types.ROBOT_POSITION.value]
|
295
|
+
parsed_map_data["robot"] = robot_data["position"]
|
296
|
+
|
297
|
+
# Parse path data with coordinate transformation FIRST
|
298
|
+
transformed_path_points = []
|
299
|
+
if self.Types.PATH.value in blocks:
|
300
|
+
path_data = blocks[self.Types.PATH.value].copy()
|
301
|
+
# Apply coordinate transformation like current parser
|
302
|
+
transformed_path_points = [
|
303
|
+
[point[0], self.Tools.DIMENSION_MM - point[1]]
|
304
|
+
for point in path_data["points"]
|
305
|
+
]
|
306
|
+
path_data["points"] = transformed_path_points
|
307
|
+
|
308
|
+
# Calculate current angle from transformed points
|
309
|
+
if len(transformed_path_points) >= 2:
|
310
|
+
last_point = transformed_path_points[-1]
|
311
|
+
second_last = transformed_path_points[-2]
|
312
|
+
dx = last_point[0] - second_last[0]
|
313
|
+
dy = last_point[1] - second_last[1]
|
314
|
+
if dx != 0 or dy != 0:
|
315
|
+
angle_rad = math.atan2(dy, dx)
|
316
|
+
path_data["current_angle"] = math.degrees(angle_rad)
|
317
|
+
parsed_map_data["path"] = path_data
|
318
|
+
|
319
|
+
# Get robot angle from TRANSFORMED path data (like current implementation)
|
320
|
+
robot_angle = 0
|
321
|
+
if len(transformed_path_points) >= 2:
|
322
|
+
last_point = transformed_path_points[-1]
|
323
|
+
second_last = transformed_path_points[-2]
|
324
|
+
dx = last_point[0] - second_last[0]
|
325
|
+
dy = last_point[1] - second_last[1]
|
326
|
+
if dx != 0 or dy != 0:
|
327
|
+
angle_rad = math.atan2(dy, dx)
|
328
|
+
robot_angle = int(math.degrees(angle_rad))
|
329
|
+
|
330
|
+
parsed_map_data["robot_angle"] = robot_angle
|
331
|
+
|
332
|
+
# Parse charger position
|
333
|
+
if self.Types.CHARGER_LOCATION.value in blocks:
|
334
|
+
charger_data = blocks[self.Types.CHARGER_LOCATION.value]
|
335
|
+
parsed_map_data["charger"] = charger_data["position"]
|
336
|
+
|
337
|
+
# Parse image data
|
338
|
+
if self.Types.IMAGE.value in blocks:
|
339
|
+
parsed_map_data["image"] = blocks[self.Types.IMAGE.value]
|
340
|
+
|
341
|
+
# Parse goto predicted path
|
342
|
+
if self.Types.GOTO_PREDICTED_PATH.value in blocks:
|
343
|
+
goto_path_data = blocks[self.Types.GOTO_PREDICTED_PATH.value].copy()
|
344
|
+
# Apply coordinate transformation
|
345
|
+
goto_path_data["points"] = [
|
346
|
+
[point[0], self.Tools.DIMENSION_MM - point[1]]
|
347
|
+
for point in goto_path_data["points"]
|
348
|
+
]
|
349
|
+
# Calculate current angle from transformed points (like working parser)
|
350
|
+
if len(goto_path_data["points"]) >= 2:
|
351
|
+
points = goto_path_data["points"]
|
352
|
+
last_point = points[-1]
|
353
|
+
second_last = points[-2]
|
354
|
+
dx = last_point[0] - second_last[0]
|
355
|
+
dy = last_point[1] - second_last[1]
|
356
|
+
if dx != 0 or dy != 0:
|
357
|
+
angle_rad = math.atan2(dy, dx)
|
358
|
+
goto_path_data["current_angle"] = math.degrees(angle_rad)
|
359
|
+
parsed_map_data["goto_predicted_path"] = goto_path_data
|
360
|
+
|
361
|
+
# Parse goto target
|
362
|
+
if self.Types.GOTO_TARGET.value in blocks:
|
363
|
+
parsed_map_data["goto_target"] = blocks[self.Types.GOTO_TARGET.value][
|
364
|
+
"position"
|
365
|
+
]
|
366
|
+
|
367
|
+
# Add missing fields to match expected JSON format
|
368
|
+
parsed_map_data["forbidden_zones"] = []
|
369
|
+
parsed_map_data["virtual_walls"] = []
|
370
|
+
|
371
|
+
return parsed_map_data
|
372
|
+
|
373
|
+
except (struct.error, IndexError, ValueError):
|
374
|
+
return None
|
375
|
+
|
376
|
+
def parse_data(
|
377
|
+
self, payload: Optional[bytes] = None, pixels: bool = False
|
378
|
+
) -> Optional[Dict[str, Any]]:
|
379
|
+
"""Get the map data from MQTT and return dictionary like old parsers."""
|
380
|
+
if payload:
|
381
|
+
try:
|
382
|
+
self.map_data = self.parse(payload)
|
383
|
+
parsed_data = self.parse_rrm_data(payload, pixels)
|
384
|
+
if parsed_data:
|
385
|
+
self.map_data.update(parsed_data)
|
386
|
+
# Return dictionary directly - faster!
|
387
|
+
return self.map_data
|
388
|
+
except (struct.error, IndexError, ValueError):
|
389
|
+
return None
|
390
|
+
return self.map_data
|
391
|
+
|
392
|
+
@staticmethod
|
393
|
+
def get_int32(data: bytes, address: int) -> int:
|
394
|
+
"""Get a 32-bit integer from the data - kept for compatibility."""
|
395
|
+
return struct.unpack_from("<i", data, address)[0]
|
@@ -12,7 +12,15 @@ from PIL import Image, ImageOps
|
|
12
12
|
from .drawable import Drawable
|
13
13
|
from .drawable_elements import DrawableElement, DrawingConfig
|
14
14
|
from .enhanced_drawable import EnhancedDrawable
|
15
|
-
from .types import
|
15
|
+
from .types import (
|
16
|
+
LOGGER,
|
17
|
+
ChargerPosition,
|
18
|
+
ImageSize,
|
19
|
+
NumpyArray,
|
20
|
+
PilPNG,
|
21
|
+
RobotPosition,
|
22
|
+
WebPBytes,
|
23
|
+
)
|
16
24
|
|
17
25
|
|
18
26
|
@dataclass
|
@@ -843,10 +851,8 @@ async def async_extract_room_outline(
|
|
843
851
|
|
844
852
|
|
845
853
|
async def numpy_to_webp_bytes(
|
846
|
-
img_np_array: np.ndarray,
|
847
|
-
|
848
|
-
lossless: bool = False
|
849
|
-
) -> bytes:
|
854
|
+
img_np_array: np.ndarray, quality: int = 85, lossless: bool = False
|
855
|
+
) -> WebPBytes:
|
850
856
|
"""
|
851
857
|
Convert NumPy array directly to WebP bytes.
|
852
858
|
|
@@ -864,13 +870,12 @@ async def numpy_to_webp_bytes(
|
|
864
870
|
# Create bytes buffer
|
865
871
|
webp_buffer = io.BytesIO()
|
866
872
|
|
867
|
-
# Save as WebP
|
873
|
+
# Save as WebP - PIL images should use lossless=True for best results
|
868
874
|
pil_img.save(
|
869
875
|
webp_buffer,
|
870
|
-
format=
|
871
|
-
|
872
|
-
|
873
|
-
method=6 # Best compression method
|
876
|
+
format="WEBP",
|
877
|
+
lossless=True, # Always lossless for PIL images
|
878
|
+
method=1, # Fastest method for lossless
|
874
879
|
)
|
875
880
|
|
876
881
|
# Get bytes and cleanup
|
@@ -881,9 +886,7 @@ async def numpy_to_webp_bytes(
|
|
881
886
|
|
882
887
|
|
883
888
|
async def pil_to_webp_bytes(
|
884
|
-
pil_img: Image.Image,
|
885
|
-
quality: int = 85,
|
886
|
-
lossless: bool = False
|
889
|
+
pil_img: Image.Image, quality: int = 85, lossless: bool = False
|
887
890
|
) -> bytes:
|
888
891
|
"""
|
889
892
|
Convert PIL Image to WebP bytes.
|
@@ -899,13 +902,12 @@ async def pil_to_webp_bytes(
|
|
899
902
|
# Create bytes buffer
|
900
903
|
webp_buffer = io.BytesIO()
|
901
904
|
|
902
|
-
# Save as WebP
|
905
|
+
# Save as WebP - PIL images should use lossless=True for best results
|
903
906
|
pil_img.save(
|
904
907
|
webp_buffer,
|
905
|
-
format=
|
906
|
-
|
907
|
-
|
908
|
-
method=6 # Best compression method
|
908
|
+
format="WEBP",
|
909
|
+
lossless=True, # Always lossless for PIL images
|
910
|
+
method=1, # Fastest method for lossless
|
909
911
|
)
|
910
912
|
|
911
913
|
# Get bytes and cleanup
|
@@ -379,11 +379,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
379
379
|
# Return WebP bytes or PIL Image based on parameter
|
380
380
|
if return_webp:
|
381
381
|
from .config.utils import pil_to_webp_bytes
|
382
|
-
|
383
|
-
|
384
|
-
quality=90,
|
385
|
-
lossless=False
|
386
|
-
)
|
382
|
+
|
383
|
+
webp_bytes = await pil_to_webp_bytes(resized_image)
|
387
384
|
return webp_bytes
|
388
385
|
else:
|
389
386
|
return resized_image
|
@@ -391,11 +388,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
391
388
|
# Return WebP bytes or PIL Image based on parameter
|
392
389
|
if return_webp:
|
393
390
|
# Convert directly from NumPy to WebP for better performance
|
394
|
-
webp_bytes = await numpy_to_webp_bytes(
|
395
|
-
img_np_array,
|
396
|
-
quality=90,
|
397
|
-
lossless=False
|
398
|
-
)
|
391
|
+
webp_bytes = await numpy_to_webp_bytes(img_np_array)
|
399
392
|
del img_np_array
|
400
393
|
LOGGER.debug("%s: Frame Completed.", self.file_name)
|
401
394
|
return webp_bytes
|
@@ -191,11 +191,7 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
191
191
|
# Return WebP bytes or PIL Image based on parameter
|
192
192
|
if return_webp:
|
193
193
|
# Convert directly to WebP bytes for better performance
|
194
|
-
webp_bytes = await numpy_to_webp_bytes(
|
195
|
-
img_np_array,
|
196
|
-
quality=90, # High quality for vacuum maps
|
197
|
-
lossless=False # Use lossy compression for smaller size
|
198
|
-
)
|
194
|
+
webp_bytes = await numpy_to_webp_bytes(img_np_array)
|
199
195
|
del img_np_array # free memory
|
200
196
|
return webp_bytes
|
201
197
|
else:
|
File without changes
|
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/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
|
{valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/hypfer_draw.py
RENAMED
File without changes
|
File without changes
|
{valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/map_data.py
RENAMED
File without changes
|
{valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/py.typed
RENAMED
File without changes
|
{valetudo_map_parser-0.1.9b57 → valetudo_map_parser-0.1.9b59}/SCR/valetudo_map_parser/reimg_draw.py
RENAMED
File without changes
|
File without changes
|