valetudo-map-parser 0.1.2__py3-none-any.whl → 0.1.4__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/__init__.py +32 -0
- valetudo_map_parser/config/__init__.py +1 -0
- valetudo_map_parser/config/auto_crop.py +288 -0
- valetudo_map_parser/config/colors.py +178 -0
- valetudo_map_parser/config/drawable.py +561 -0
- valetudo_map_parser/config/shared.py +249 -0
- valetudo_map_parser/config/types.py +590 -0
- valetudo_map_parser/hypfer_draw.py +422 -0
- valetudo_map_parser/hypfer_handler.py +418 -0
- valetudo_map_parser/images_utils.py +398 -0
- valetudo_map_parser/map_data.py +510 -0
- valetudo_map_parser/py.typed +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.4.dist-info}/METADATA +2 -2
- valetudo_map_parser-0.1.4.dist-info/RECORD +17 -0
- __init__.py +0 -18
- valetudo_map_parser-0.1.2.dist-info/RECORD +0 -6
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.4.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.4.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.2.dist-info → valetudo_map_parser-0.1.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,422 @@
|
|
1
|
+
"""
|
2
|
+
Image Draw Class for Valetudo Hypfer Image Handling.
|
3
|
+
This class is used to simplify the ImageHandler class.
|
4
|
+
Version: 2024.07.2
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import hashlib
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from .config.types import (
|
14
|
+
Color,
|
15
|
+
JsonType,
|
16
|
+
NumpyArray,
|
17
|
+
RobotPosition,
|
18
|
+
)
|
19
|
+
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class ImageDraw:
|
24
|
+
"""Class to handle the image creation.
|
25
|
+
It Draws each element of the images, like the walls, zones, paths, etc."""
|
26
|
+
|
27
|
+
def __init__(self, image_handler):
|
28
|
+
self.img_h = image_handler
|
29
|
+
self.file_name = self.img_h.shared.file_name
|
30
|
+
|
31
|
+
async def draw_go_to_flag(
|
32
|
+
self, np_array: NumpyArray, entity_dict: dict, color_go_to: Color
|
33
|
+
) -> NumpyArray:
|
34
|
+
"""Draw the goto target flag on the map."""
|
35
|
+
go_to = entity_dict.get("go_to_target")
|
36
|
+
if go_to:
|
37
|
+
np_array = await self.img_h.draw.go_to_flag(
|
38
|
+
np_array,
|
39
|
+
(go_to[0]["points"][0], go_to[0]["points"][1]),
|
40
|
+
self.img_h.shared.image_rotate,
|
41
|
+
color_go_to,
|
42
|
+
)
|
43
|
+
return np_array
|
44
|
+
|
45
|
+
async def async_draw_base_layer(
|
46
|
+
self,
|
47
|
+
img_np_array,
|
48
|
+
compressed_pixels_list,
|
49
|
+
layer_type,
|
50
|
+
color_wall,
|
51
|
+
color_zone_clean,
|
52
|
+
pixel_size,
|
53
|
+
):
|
54
|
+
"""Draw the base layer of the map."""
|
55
|
+
room_id = 0
|
56
|
+
|
57
|
+
for compressed_pixels in compressed_pixels_list:
|
58
|
+
pixels = self.img_h.data.sublist(compressed_pixels, 3)
|
59
|
+
|
60
|
+
if layer_type in ["segment", "floor"]:
|
61
|
+
img_np_array, room_id = await self._process_room_layer(
|
62
|
+
img_np_array,
|
63
|
+
pixels,
|
64
|
+
layer_type,
|
65
|
+
room_id,
|
66
|
+
pixel_size,
|
67
|
+
color_zone_clean,
|
68
|
+
)
|
69
|
+
elif layer_type == "wall":
|
70
|
+
img_np_array = await self._process_wall_layer(
|
71
|
+
img_np_array, pixels, pixel_size, color_wall
|
72
|
+
)
|
73
|
+
|
74
|
+
return room_id, img_np_array
|
75
|
+
|
76
|
+
async def _process_room_layer(
|
77
|
+
self, img_np_array, pixels, layer_type, room_id, pixel_size, color_zone_clean
|
78
|
+
):
|
79
|
+
"""Process a room layer (segment or floor)."""
|
80
|
+
room_color = self.img_h.rooms_colors[room_id]
|
81
|
+
|
82
|
+
try:
|
83
|
+
if layer_type == "segment":
|
84
|
+
room_color = self._get_active_room_color(
|
85
|
+
room_id, room_color, color_zone_clean
|
86
|
+
)
|
87
|
+
|
88
|
+
img_np_array = await self.img_h.draw.from_json_to_image(
|
89
|
+
img_np_array, pixels, pixel_size, room_color
|
90
|
+
)
|
91
|
+
room_id = (room_id + 1) % 16 # Cycle room_id back to 0 after 15
|
92
|
+
|
93
|
+
except IndexError as e:
|
94
|
+
_LOGGER.warning("%s: Image Draw Error: %s", self.file_name, str(e))
|
95
|
+
_LOGGER.debug(
|
96
|
+
"%s Active Zones: %s and Room ID: %s",
|
97
|
+
self.file_name,
|
98
|
+
str(self.img_h.active_zones),
|
99
|
+
str(room_id),
|
100
|
+
)
|
101
|
+
|
102
|
+
return img_np_array, room_id
|
103
|
+
|
104
|
+
def _get_active_room_color(self, room_id, room_color, color_zone_clean):
|
105
|
+
"""Adjust the room color if the room is active."""
|
106
|
+
if self.img_h.active_zones and room_id < len(self.img_h.active_zones):
|
107
|
+
if self.img_h.active_zones[room_id] == 1:
|
108
|
+
return tuple(
|
109
|
+
((2 * room_color[i]) + color_zone_clean[i]) // 3 for i in range(4)
|
110
|
+
)
|
111
|
+
return room_color
|
112
|
+
|
113
|
+
async def _process_wall_layer(self, img_np_array, pixels, pixel_size, color_wall):
|
114
|
+
"""Process a wall layer."""
|
115
|
+
return await self.img_h.draw.from_json_to_image(
|
116
|
+
img_np_array, pixels, pixel_size, color_wall
|
117
|
+
)
|
118
|
+
|
119
|
+
async def async_draw_obstacle(
|
120
|
+
self, np_array: NumpyArray, entity_dict: dict, color_no_go: Color
|
121
|
+
) -> NumpyArray:
|
122
|
+
"""Get the obstacle positions from the entity data."""
|
123
|
+
try:
|
124
|
+
obstacle_data = entity_dict.get("obstacle")
|
125
|
+
except KeyError:
|
126
|
+
_LOGGER.info("%s No obstacle found.", self.file_name)
|
127
|
+
else:
|
128
|
+
obstacle_positions = []
|
129
|
+
if obstacle_data:
|
130
|
+
for obstacle in obstacle_data:
|
131
|
+
label = obstacle.get("metaData", {}).get("label")
|
132
|
+
points = obstacle.get("points", [])
|
133
|
+
|
134
|
+
if label and points:
|
135
|
+
obstacle_pos = {
|
136
|
+
"label": label,
|
137
|
+
"points": {"x": points[0], "y": points[1]},
|
138
|
+
}
|
139
|
+
obstacle_positions.append(obstacle_pos)
|
140
|
+
|
141
|
+
# List of dictionaries containing label and points for each obstacle
|
142
|
+
# and draw obstacles on the map
|
143
|
+
if obstacle_positions:
|
144
|
+
self.img_h.draw.draw_obstacles(
|
145
|
+
np_array, obstacle_positions, color_no_go
|
146
|
+
)
|
147
|
+
return np_array
|
148
|
+
|
149
|
+
async def async_draw_charger(
|
150
|
+
self,
|
151
|
+
np_array: NumpyArray,
|
152
|
+
entity_dict: dict,
|
153
|
+
color_charger: Color,
|
154
|
+
) -> NumpyArray:
|
155
|
+
"""Get the charger position from the entity data."""
|
156
|
+
try:
|
157
|
+
charger_pos = entity_dict.get("charger_location")
|
158
|
+
except KeyError:
|
159
|
+
_LOGGER.warning("%s: No charger position found.", self.file_name)
|
160
|
+
else:
|
161
|
+
if charger_pos:
|
162
|
+
charger_pos = charger_pos[0]["points"]
|
163
|
+
self.img_h.charger_pos = {
|
164
|
+
"x": charger_pos[0],
|
165
|
+
"y": charger_pos[1],
|
166
|
+
}
|
167
|
+
np_array = await self.img_h.draw.battery_charger(
|
168
|
+
np_array, charger_pos[0], charger_pos[1], color_charger
|
169
|
+
)
|
170
|
+
return np_array
|
171
|
+
return np_array
|
172
|
+
|
173
|
+
async def async_get_json_id(self, my_json: JsonType) -> str | None:
|
174
|
+
"""Return the JSON ID from the image."""
|
175
|
+
try:
|
176
|
+
json_id = my_json["metaData"]["nonce"]
|
177
|
+
except (ValueError, KeyError) as e:
|
178
|
+
_LOGGER.debug("%s: No JsonID provided: %s", self.file_name, str(e))
|
179
|
+
json_id = None
|
180
|
+
return json_id
|
181
|
+
|
182
|
+
async def async_draw_zones(
|
183
|
+
self,
|
184
|
+
m_json: JsonType,
|
185
|
+
np_array: NumpyArray,
|
186
|
+
color_zone_clean: Color,
|
187
|
+
color_no_go: Color,
|
188
|
+
) -> NumpyArray:
|
189
|
+
"""Get the zone clean from the JSON data."""
|
190
|
+
try:
|
191
|
+
zone_clean = self.img_h.data.find_zone_entities(m_json)
|
192
|
+
except (ValueError, KeyError):
|
193
|
+
zone_clean = None
|
194
|
+
else:
|
195
|
+
_LOGGER.info("%s: Got zones.", self.file_name)
|
196
|
+
if zone_clean:
|
197
|
+
try:
|
198
|
+
zones_active = zone_clean.get("active_zone")
|
199
|
+
except KeyError:
|
200
|
+
zones_active = None
|
201
|
+
if zones_active:
|
202
|
+
np_array = await self.img_h.draw.zones(
|
203
|
+
np_array, zones_active, color_zone_clean
|
204
|
+
)
|
205
|
+
try:
|
206
|
+
no_go_zones = zone_clean.get("no_go_area")
|
207
|
+
except KeyError:
|
208
|
+
no_go_zones = None
|
209
|
+
|
210
|
+
if no_go_zones:
|
211
|
+
np_array = await self.img_h.draw.zones(
|
212
|
+
np_array, no_go_zones, color_no_go
|
213
|
+
)
|
214
|
+
|
215
|
+
try:
|
216
|
+
no_mop_zones = zone_clean.get("no_mop_area")
|
217
|
+
except KeyError:
|
218
|
+
no_mop_zones = None
|
219
|
+
|
220
|
+
if no_mop_zones:
|
221
|
+
np_array = await self.img_h.draw.zones(
|
222
|
+
np_array, no_mop_zones, color_no_go
|
223
|
+
)
|
224
|
+
return np_array
|
225
|
+
|
226
|
+
async def async_draw_virtual_walls(
|
227
|
+
self, m_json: JsonType, np_array: NumpyArray, color_no_go: Color
|
228
|
+
) -> NumpyArray:
|
229
|
+
"""Get the virtual walls from the JSON data."""
|
230
|
+
try:
|
231
|
+
virtual_walls = self.img_h.data.find_virtual_walls(m_json)
|
232
|
+
except (ValueError, KeyError):
|
233
|
+
virtual_walls = None
|
234
|
+
else:
|
235
|
+
_LOGGER.info("%s: Got virtual walls.", self.file_name)
|
236
|
+
if virtual_walls:
|
237
|
+
np_array = await self.img_h.draw.draw_virtual_walls(
|
238
|
+
np_array, virtual_walls, color_no_go
|
239
|
+
)
|
240
|
+
return np_array
|
241
|
+
|
242
|
+
async def async_draw_paths(
|
243
|
+
self,
|
244
|
+
np_array: NumpyArray,
|
245
|
+
m_json: JsonType,
|
246
|
+
color_move: Color,
|
247
|
+
color_gray: Color,
|
248
|
+
) -> NumpyArray:
|
249
|
+
"""Get the paths from the JSON data."""
|
250
|
+
# Initialize the variables
|
251
|
+
path_pixels = None
|
252
|
+
predicted_path = None
|
253
|
+
# Extract the paths data from the JSON data.
|
254
|
+
try:
|
255
|
+
paths_data = self.img_h.data.find_paths_entities(m_json)
|
256
|
+
predicted_path = paths_data.get("predicted_path", [])
|
257
|
+
path_pixels = paths_data.get("path", [])
|
258
|
+
except KeyError as e:
|
259
|
+
_LOGGER.warning("%s: Error extracting paths data:", str(e))
|
260
|
+
|
261
|
+
if predicted_path:
|
262
|
+
predicted_path = predicted_path[0]["points"]
|
263
|
+
predicted_path = self.img_h.data.sublist(predicted_path, 2)
|
264
|
+
predicted_pat2 = self.img_h.data.sublist_join(predicted_path, 2)
|
265
|
+
np_array = await self.img_h.draw.lines(
|
266
|
+
np_array, predicted_pat2, 2, color_gray
|
267
|
+
)
|
268
|
+
if path_pixels:
|
269
|
+
for path in path_pixels:
|
270
|
+
# Get the points from the current path and extend multiple paths.
|
271
|
+
points = path.get("points", [])
|
272
|
+
sublists = self.img_h.data.sublist(points, 2)
|
273
|
+
self.img_h.shared.map_new_path = self.img_h.data.sublist_join(
|
274
|
+
sublists, 2
|
275
|
+
)
|
276
|
+
np_array = await self.img_h.draw.lines(
|
277
|
+
np_array, self.img_h.shared.map_new_path, 5, color_move
|
278
|
+
)
|
279
|
+
return np_array
|
280
|
+
|
281
|
+
async def async_get_entity_data(self, m_json: JsonType) -> dict or None:
|
282
|
+
"""Get the entity data from the JSON data."""
|
283
|
+
try:
|
284
|
+
entity_dict = self.img_h.data.find_points_entities(m_json)
|
285
|
+
except (ValueError, KeyError):
|
286
|
+
entity_dict = None
|
287
|
+
else:
|
288
|
+
_LOGGER.info("%s: Got the points in the json.", self.file_name)
|
289
|
+
return entity_dict
|
290
|
+
|
291
|
+
@staticmethod
|
292
|
+
async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
|
293
|
+
"""Copy the array."""
|
294
|
+
return NumpyArray.copy(original_array)
|
295
|
+
|
296
|
+
async def calculate_array_hash(self, layers: dict, active: list[int] = None) -> str:
|
297
|
+
"""Calculate the hash of the image based on the layers and active segments walls."""
|
298
|
+
self.img_h.active_zones = active
|
299
|
+
if layers and active:
|
300
|
+
data_to_hash = {
|
301
|
+
"layers": len(layers["wall"][0]),
|
302
|
+
"active_segments": tuple(active),
|
303
|
+
}
|
304
|
+
data_json = json.dumps(data_to_hash, sort_keys=True)
|
305
|
+
hash_value = hashlib.sha256(data_json.encode()).hexdigest()
|
306
|
+
else:
|
307
|
+
hash_value = None
|
308
|
+
return hash_value
|
309
|
+
|
310
|
+
async def async_get_robot_in_room(
|
311
|
+
self, robot_y: int = 0, robot_x: int = 0, angle: float = 0.0
|
312
|
+
) -> RobotPosition:
|
313
|
+
"""Get the robot position and return in what room is."""
|
314
|
+
if self.img_h.robot_in_room:
|
315
|
+
# Check if the robot coordinates are inside the room's corners
|
316
|
+
if (
|
317
|
+
(self.img_h.robot_in_room["right"] >= int(robot_x))
|
318
|
+
and (self.img_h.robot_in_room["left"] <= int(robot_x))
|
319
|
+
) and (
|
320
|
+
(self.img_h.robot_in_room["down"] >= int(robot_y))
|
321
|
+
and (self.img_h.robot_in_room["up"] <= int(robot_y))
|
322
|
+
):
|
323
|
+
temp = {
|
324
|
+
"x": robot_x,
|
325
|
+
"y": robot_y,
|
326
|
+
"angle": angle,
|
327
|
+
"in_room": self.img_h.robot_in_room["room"],
|
328
|
+
}
|
329
|
+
if self.img_h.active_zones and (
|
330
|
+
self.img_h.robot_in_room["id"]
|
331
|
+
in range(len(self.img_h.active_zones))
|
332
|
+
): # issue #100 Index out of range.
|
333
|
+
self.img_h.zooming = bool(
|
334
|
+
self.img_h.active_zones[self.img_h.robot_in_room["id"]]
|
335
|
+
)
|
336
|
+
else:
|
337
|
+
self.img_h.zooming = False
|
338
|
+
return temp
|
339
|
+
# else we need to search and use the async method.
|
340
|
+
if self.img_h.rooms_pos:
|
341
|
+
last_room = None
|
342
|
+
room_count = 0
|
343
|
+
if self.img_h.robot_in_room:
|
344
|
+
last_room = self.img_h.robot_in_room
|
345
|
+
for room in self.img_h.rooms_pos:
|
346
|
+
corners = room["corners"]
|
347
|
+
self.img_h.robot_in_room = {
|
348
|
+
"id": room_count,
|
349
|
+
"left": int(corners[0][0]),
|
350
|
+
"right": int(corners[2][0]),
|
351
|
+
"up": int(corners[0][1]),
|
352
|
+
"down": int(corners[2][1]),
|
353
|
+
"room": str(room["name"]),
|
354
|
+
}
|
355
|
+
room_count += 1
|
356
|
+
# Check if the robot coordinates are inside the room's corners
|
357
|
+
if (
|
358
|
+
(self.img_h.robot_in_room["right"] >= int(robot_x))
|
359
|
+
and (self.img_h.robot_in_room["left"] <= int(robot_x))
|
360
|
+
) and (
|
361
|
+
(self.img_h.robot_in_room["down"] >= int(robot_y))
|
362
|
+
and (self.img_h.robot_in_room["up"] <= int(robot_y))
|
363
|
+
):
|
364
|
+
temp = {
|
365
|
+
"x": robot_x,
|
366
|
+
"y": robot_y,
|
367
|
+
"angle": angle,
|
368
|
+
"in_room": self.img_h.robot_in_room["room"],
|
369
|
+
}
|
370
|
+
_LOGGER.debug(
|
371
|
+
"%s is in %s room.",
|
372
|
+
self.file_name,
|
373
|
+
self.img_h.robot_in_room["room"],
|
374
|
+
)
|
375
|
+
del room, corners, robot_x, robot_y # free memory.
|
376
|
+
return temp
|
377
|
+
del room, corners # free memory.
|
378
|
+
_LOGGER.debug(
|
379
|
+
"%s not located within Camera Rooms coordinates.",
|
380
|
+
self.file_name,
|
381
|
+
)
|
382
|
+
self.img_h.robot_in_room = last_room
|
383
|
+
self.img_h.zooming = False
|
384
|
+
temp = {
|
385
|
+
"x": robot_x,
|
386
|
+
"y": robot_y,
|
387
|
+
"angle": angle,
|
388
|
+
"in_room": last_room["room"] if last_room else None,
|
389
|
+
}
|
390
|
+
# If the robot is not inside any room, return a default value
|
391
|
+
return temp
|
392
|
+
|
393
|
+
async def async_get_robot_position(self, entity_dict: dict) -> tuple | None:
|
394
|
+
"""Get the robot position from the entity data."""
|
395
|
+
robot_pos = None
|
396
|
+
robot_position = None
|
397
|
+
robot_position_angle = None
|
398
|
+
try:
|
399
|
+
robot_pos = entity_dict.get("robot_position")
|
400
|
+
except KeyError:
|
401
|
+
_LOGGER.warning("%s No robot position found.", self.file_name)
|
402
|
+
return None, None, None
|
403
|
+
finally:
|
404
|
+
if robot_pos:
|
405
|
+
robot_position = robot_pos[0]["points"]
|
406
|
+
robot_position_angle = round(
|
407
|
+
float(robot_pos[0]["metaData"]["angle"]), 1
|
408
|
+
)
|
409
|
+
if self.img_h.rooms_pos is None:
|
410
|
+
self.img_h.robot_pos = {
|
411
|
+
"x": robot_position[0],
|
412
|
+
"y": robot_position[1],
|
413
|
+
"angle": robot_position_angle,
|
414
|
+
}
|
415
|
+
else:
|
416
|
+
self.img_h.robot_pos = await self.async_get_robot_in_room(
|
417
|
+
robot_y=(robot_position[1]),
|
418
|
+
robot_x=(robot_position[0]),
|
419
|
+
angle=robot_position_angle,
|
420
|
+
)
|
421
|
+
|
422
|
+
return robot_pos, robot_position, robot_position_angle
|