valetudo-map-parser 0.1.9b5__tar.gz → 0.1.9b7__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.
Files changed (22) hide show
  1. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/PKG-INFO +1 -1
  2. valetudo_map_parser-0.1.9b7/SCR/valetudo_map_parser/config/utils.py +448 -0
  3. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/hypfer_draw.py +1 -1
  4. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/hypfer_handler.py +4 -6
  5. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/rand25_handler.py +3 -5
  6. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/reimg_draw.py +0 -2
  7. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/pyproject.toml +1 -1
  8. valetudo_map_parser-0.1.9b5/SCR/valetudo_map_parser/config/utils.py +0 -132
  9. valetudo_map_parser-0.1.9b5/SCR/valetudo_map_parser/images_utils.py +0 -335
  10. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/LICENSE +0 -0
  11. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/NOTICE.txt +0 -0
  12. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/README.md +0 -0
  13. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/__init__.py +0 -0
  14. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  15. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  16. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/colors.py +0 -0
  17. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/drawable.py +0 -0
  18. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
  19. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/shared.py +0 -0
  20. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/config/types.py +0 -0
  21. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/map_data.py +0 -0
  22. {valetudo_map_parser-0.1.9b5 → valetudo_map_parser-0.1.9b7}/SCR/valetudo_map_parser/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b5
3
+ Version: 0.1.9b7
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
@@ -0,0 +1,448 @@
1
+ """Utility code for the valetudo map parser."""
2
+
3
+ import hashlib
4
+ import json
5
+ from logging import getLogger
6
+
7
+ from PIL import ImageOps
8
+
9
+ from .types import ChargerPosition, ImageSize, NumpyArray, RobotPosition
10
+
11
+ _LOGGER = getLogger(__name__)
12
+
13
+
14
+ class BaseHandler:
15
+ """Avoid Code duplication"""
16
+
17
+ def __init__(self):
18
+ self.file_name = None
19
+ self.img_size = None
20
+ self.json_data = None
21
+ self.json_id = None
22
+ self.path_pixels = None
23
+ self.robot_in_room = None
24
+ self.robot_pos = None
25
+ self.room_propriety = None
26
+ self.rooms_pos = None
27
+ self.charger_pos = None
28
+ self.frame_number = 0
29
+ self.max_frames = 1024
30
+ self.crop_img_size = [0, 0]
31
+ self.offset_x = 0
32
+ self.offset_y = 0
33
+ self.shared = None
34
+
35
+ def get_frame_number(self) -> int:
36
+ """Return the frame number of the image."""
37
+ return self.frame_number
38
+
39
+ def get_robot_position(self) -> RobotPosition | None:
40
+ """Return the robot position."""
41
+ return self.robot_pos
42
+
43
+ def get_charger_position(self) -> ChargerPosition | None:
44
+ """Return the charger position."""
45
+ return self.charger_pos
46
+
47
+ def get_img_size(self) -> ImageSize | None:
48
+ """Return the size of the image."""
49
+ return self.img_size
50
+
51
+ def get_json_id(self) -> str | None:
52
+ """Return the JSON ID from the image."""
53
+ return self.json_id
54
+
55
+ async def async_resize_image(
56
+ self, pil_img, width, height, aspect_ratio=None, is_rand=False
57
+ ):
58
+ """Resize the image to the given dimensions and aspect ratio."""
59
+ if aspect_ratio:
60
+ wsf, hsf = [int(x) for x in aspect_ratio.split(",")]
61
+ if wsf == 0 or hsf == 0:
62
+ return pil_img
63
+ new_aspect_ratio = wsf / hsf
64
+ if width / height > new_aspect_ratio:
65
+ new_width = int(pil_img.height * new_aspect_ratio)
66
+ new_height = pil_img.height
67
+ else:
68
+ new_width = pil_img.width
69
+ new_height = int(pil_img.width / new_aspect_ratio)
70
+ _LOGGER.debug(
71
+ "%s: Image Aspect Ratio: %s, %s",
72
+ self.file_name,
73
+ str(wsf),
74
+ str(hsf),
75
+ )
76
+ (
77
+ self.crop_img_size[0],
78
+ self.crop_img_size[1],
79
+ ) = await self.async_map_coordinates_offset(
80
+ wsf, hsf, new_width, new_height, is_rand
81
+ )
82
+ return ImageOps.pad(pil_img, (new_width, new_height))
83
+ return ImageOps.pad(pil_img, (width, height))
84
+
85
+ async def async_map_coordinates_offset(
86
+ self, wsf: int, hsf: int, width: int, height: int, rand256: bool = False
87
+ ) -> tuple[int, int]:
88
+ """
89
+ Offset the coordinates to the map.
90
+ """
91
+
92
+ if wsf == 1 and hsf == 1:
93
+ self.set_image_offset_ratio_1_1(width, height, rand256)
94
+ elif wsf == 2 and hsf == 1:
95
+ self.set_image_offset_ratio_2_1(width, height, rand256)
96
+ elif wsf == 3 and hsf == 2:
97
+ self.set_image_offset_ratio_3_2(width, height, rand256)
98
+ elif wsf == 5 and hsf == 4:
99
+ self.set_image_offset_ratio_5_4(width, height, rand256)
100
+ elif wsf == 9 and hsf == 16:
101
+ self.set_image_offset_ratio_9_16(width, height, rand256)
102
+ elif wsf == 16 and hsf == 9:
103
+ self.set_image_offset_ratio_16_9(width, height, rand256)
104
+ return width, height
105
+
106
+ @staticmethod
107
+ async def calculate_array_hash(layers: dict, active: list[int] = None) -> str:
108
+ """Calculate the hash of the image based on layers and active zones."""
109
+ if layers and active:
110
+ data_to_hash = {
111
+ "layers": len(layers["wall"][0]),
112
+ "active_segments": tuple(active),
113
+ }
114
+ data_json = json.dumps(data_to_hash, sort_keys=True)
115
+ return hashlib.sha256(data_json.encode()).hexdigest()
116
+ return None
117
+
118
+ @staticmethod
119
+ async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
120
+ """Copy the array."""
121
+ return NumpyArray.copy(original_array)
122
+
123
+ def get_map_points(self) -> dict:
124
+ """Return the map points."""
125
+ return [
126
+ {"x": 0, "y": 0}, # Top-left corner 0
127
+ {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
128
+ {
129
+ "x": self.crop_img_size[0],
130
+ "y": self.crop_img_size[1],
131
+ }, # Bottom-right corner 2
132
+ {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
133
+ ]
134
+
135
+ def set_image_offset_ratio_1_1(
136
+ self, width: int, height: int, rand256: bool = False
137
+ ) -> None:
138
+ """Set the image offset ratio to 1:1."""
139
+
140
+ rotation = self.shared.image_rotate
141
+ if not rand256:
142
+ if rotation in [0, 180]:
143
+ self.offset_y = self.crop_img_size[0] - width
144
+ self.offset_x = (height - self.crop_img_size[1]) // 2
145
+ elif rotation in [90, 270]:
146
+ self.offset_y = width - self.crop_img_size[0]
147
+ self.offset_x = (self.crop_img_size[1] - height) // 2
148
+ else:
149
+ if rotation in [0, 180]:
150
+ self.offset_x = (width - self.crop_img_size[0]) // 2
151
+ self.offset_y = height - self.crop_img_size[1]
152
+ elif rotation in [90, 270]:
153
+ self.offset_y = (self.crop_img_size[0] - width) // 2
154
+ self.offset_x = self.crop_img_size[1] - height
155
+ _LOGGER.debug(
156
+ "%s Image Coordinates Offsets (x,y): %s. %s",
157
+ self.file_name,
158
+ self.offset_x,
159
+ self.offset_y,
160
+ )
161
+
162
+ def set_image_offset_ratio_2_1(
163
+ self, width: int, height: int, rand256: bool = False
164
+ ) -> None:
165
+ """Set the image offset ratio to 2:1."""
166
+
167
+ rotation = self.shared.image_rotate
168
+ if not rand256:
169
+ if rotation in [0, 180]:
170
+ self.offset_y = width - self.crop_img_size[0]
171
+ self.offset_x = height - self.crop_img_size[1]
172
+ elif rotation in [90, 270]:
173
+ self.offset_x = width - self.crop_img_size[0]
174
+ self.offset_y = height - self.crop_img_size[1]
175
+ else:
176
+ if rotation in [0, 180]:
177
+ self.offset_y = width - self.crop_img_size[0]
178
+ self.offset_x = height - self.crop_img_size[1]
179
+ elif rotation in [90, 270]:
180
+ self.offset_x = width - self.crop_img_size[0]
181
+ self.offset_y = height - self.crop_img_size[1]
182
+
183
+ _LOGGER.debug(
184
+ "%s Image Coordinates Offsets (x,y): %s. %s",
185
+ self.file_name,
186
+ self.offset_x,
187
+ self.offset_y,
188
+ )
189
+
190
+ def set_image_offset_ratio_3_2(
191
+ self, width: int, height: int, rand256: bool = False
192
+ ) -> None:
193
+ """Set the image offset ratio to 3:2."""
194
+
195
+ rotation = self.shared.image_rotate
196
+
197
+ if not rand256:
198
+ if rotation in [0, 180]:
199
+ self.offset_y = width - self.crop_img_size[0]
200
+ self.offset_x = ((height - self.crop_img_size[1]) // 2) - (
201
+ self.crop_img_size[1] // 10
202
+ )
203
+ elif rotation in [90, 270]:
204
+ self.offset_y = (self.crop_img_size[0] - width) // 2
205
+ self.offset_x = (self.crop_img_size[1] - height) + (
206
+ (height // 10) // 2
207
+ )
208
+ else:
209
+ if rotation in [0, 180]:
210
+ self.offset_x = (width - self.crop_img_size[0]) // 2
211
+ self.offset_y = height - self.crop_img_size[1]
212
+ elif rotation in [90, 270]:
213
+ self.offset_y = (self.crop_img_size[0] - width) // 2
214
+ self.offset_x = self.crop_img_size[1] - height
215
+
216
+ _LOGGER.debug(
217
+ "%s Image Coordinates Offsets (x,y): %s. %s",
218
+ self.file_name,
219
+ self.offset_x,
220
+ self.offset_y,
221
+ )
222
+
223
+ def set_image_offset_ratio_5_4(
224
+ self, width: int, height: int, rand256: bool = False
225
+ ) -> None:
226
+ """Set the image offset ratio to 5:4."""
227
+
228
+ rotation = self.shared.image_rotate
229
+ if not rand256:
230
+ if rotation in [0, 180]:
231
+ self.offset_x = ((width - self.crop_img_size[0]) // 2) - (
232
+ self.crop_img_size[0] // 2
233
+ )
234
+ self.offset_y = (self.crop_img_size[1] - height) - (
235
+ self.crop_img_size[1] // 2
236
+ )
237
+ elif rotation in [90, 270]:
238
+ self.offset_y = ((self.crop_img_size[0] - width) // 2) - 10
239
+ self.offset_x = (self.crop_img_size[1] - height) + (
240
+ height // 10
241
+ )
242
+ else:
243
+ if rotation in [0, 180]:
244
+ self.offset_y = (width - self.crop_img_size[0]) // 2
245
+ self.offset_x = self.crop_img_size[1] - height
246
+ elif rotation in [90, 270]:
247
+ self.offset_y = (self.crop_img_size[0] - width) // 2
248
+ self.offset_x = self.crop_img_size[1] - height
249
+
250
+ _LOGGER.debug(
251
+ "%s Image Coordinates Offsets (x,y): %s. %s",
252
+ self.file_name,
253
+ self.offset_x,
254
+ self.offset_y,
255
+ )
256
+
257
+ def set_image_offset_ratio_9_16(
258
+ self, width: int, height: int, rand256: bool = False
259
+ ) -> None:
260
+ """Set the image offset ratio to 9:16."""
261
+
262
+ rotation = self.shared.image_rotate
263
+ if not rand256:
264
+ if rotation in [0, 180]:
265
+ self.offset_y = width - self.crop_img_size[0]
266
+ self.offset_x = height - self.crop_img_size[1]
267
+ elif rotation in [90, 270]:
268
+ self.offset_x = (width - self.crop_img_size[0]) + (height // 10)
269
+ self.offset_y = height - self.crop_img_size[1]
270
+ else:
271
+ if rotation in [0, 180]:
272
+ self.offset_y = width - self.crop_img_size[0]
273
+ self.offset_x = height - self.crop_img_size[1]
274
+ elif rotation in [90, 270]:
275
+ self.offset_x = width - self.crop_img_size[0]
276
+ self.offset_y = height - self.crop_img_size[1]
277
+
278
+ _LOGGER.debug(
279
+ "%s Image Coordinates Offsets (x,y): %s. %s",
280
+ self.file_name,
281
+ self.offset_x,
282
+ self.offset_y,
283
+ )
284
+
285
+ def set_image_offset_ratio_16_9(
286
+ self, width: int, height: int, rand256: bool = False
287
+ ) -> None:
288
+ """Set the image offset ratio to 16:9."""
289
+
290
+ rotation = self.shared.image_rotate
291
+ if not rand256:
292
+ if rotation in [0, 180]:
293
+ self.offset_y = width - self.crop_img_size[0]
294
+ self.offset_x = height - self.crop_img_size[1]
295
+ elif rotation in [90, 270]:
296
+ self.offset_x = width - self.crop_img_size[0]
297
+ self.offset_y = height - self.crop_img_size[1]
298
+ else:
299
+ if rotation in [0, 180]:
300
+ self.offset_y = width - self.crop_img_size[0]
301
+ self.offset_x = height - self.crop_img_size[1]
302
+ elif rotation in [90, 270]:
303
+ self.offset_x = width - self.crop_img_size[0]
304
+ self.offset_y = height - self.crop_img_size[1]
305
+
306
+ _LOGGER.debug(
307
+ "%s Image Coordinates Offsets (x,y): %s. %s",
308
+ self.file_name,
309
+ self.offset_x,
310
+ self.offset_y,
311
+ )
312
+
313
+ def get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
314
+ """Calculate the calibration points based on the rotation angle."""
315
+
316
+ # get_calibration_data
317
+ vacuum_points = [
318
+ {
319
+ "x": self.img.crop_area[0] + self.img.offset_x,
320
+ "y": self.img.crop_area[1] + self.img.offset_y,
321
+ }, # Top-left corner 0
322
+ {
323
+ "x": self.img.crop_area[2] - self.img.offset_x,
324
+ "y": self.img.crop_area[1] + self.img.offset_y,
325
+ }, # Top-right corner 1
326
+ {
327
+ "x": self.img.crop_area[2] - self.img.offset_x,
328
+ "y": self.img.crop_area[3] - self.img.offset_y,
329
+ }, # Bottom-right corner 2
330
+ {
331
+ "x": self.img.crop_area[0] + self.img.offset_x,
332
+ "y": self.img.crop_area[3] - self.img.offset_y,
333
+ }, # Bottom-left corner (optional)3
334
+ ]
335
+
336
+ # Rotate the vacuum points based on the rotation angle
337
+ if rotation_angle == 90:
338
+ vacuum_points = [
339
+ vacuum_points[1],
340
+ vacuum_points[2],
341
+ vacuum_points[3],
342
+ vacuum_points[0],
343
+ ]
344
+ elif rotation_angle == 180:
345
+ vacuum_points = [
346
+ vacuum_points[2],
347
+ vacuum_points[3],
348
+ vacuum_points[0],
349
+ vacuum_points[1],
350
+ ]
351
+ elif rotation_angle == 270:
352
+ vacuum_points = [
353
+ vacuum_points[3],
354
+ vacuum_points[0],
355
+ vacuum_points[1],
356
+ vacuum_points[2],
357
+ ]
358
+
359
+ return vacuum_points
360
+
361
+ def re_get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
362
+ """Recalculate the calibration points based on the rotation angle.
363
+ RAND256 Vacuums Calibration Points are in 10th of a mm."""
364
+ vacuum_points = [
365
+ {
366
+ "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
367
+ "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
368
+ }, # Top-left corner 0
369
+ {
370
+ "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
371
+ "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
372
+ }, # Top-right corner 1
373
+ {
374
+ "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
375
+ "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
376
+ }, # Bottom-right corner 2
377
+ {
378
+ "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
379
+ "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
380
+ }, # Bottom-left corner (optional)3
381
+ ]
382
+
383
+ # Rotate the vacuum points based on the rotation angle
384
+ if rotation_angle == 90:
385
+ vacuum_points = [
386
+ vacuum_points[1],
387
+ vacuum_points[2],
388
+ vacuum_points[3],
389
+ vacuum_points[0],
390
+ ]
391
+ elif rotation_angle == 180:
392
+ vacuum_points = [
393
+ vacuum_points[2],
394
+ vacuum_points[3],
395
+ vacuum_points[0],
396
+ vacuum_points[1],
397
+ ]
398
+ elif rotation_angle == 270:
399
+ vacuum_points = [
400
+ vacuum_points[3],
401
+ vacuum_points[0],
402
+ vacuum_points[1],
403
+ vacuum_points[2],
404
+ ]
405
+
406
+ return vacuum_points
407
+
408
+ async def async_zone_propriety(self, zones_data) -> dict:
409
+ """Get the zone propriety"""
410
+ zone_properties = {}
411
+ id_count = 1
412
+ for zone in zones_data:
413
+ zone_name = zone.get("name")
414
+ coordinates = zone.get("coordinates")
415
+ if coordinates and len(coordinates) > 0:
416
+ coordinates[0].pop()
417
+ x1, y1, x2, y2 = coordinates[0]
418
+ zone_properties[zone_name] = {
419
+ "zones": coordinates,
420
+ "name": zone_name,
421
+ "x": ((x1 + x2) // 2),
422
+ "y": ((y1 + y2) // 2),
423
+ }
424
+ id_count += 1
425
+ if id_count > 1:
426
+ _LOGGER.debug("%s: Zones Properties updated.", self.file_name)
427
+ return zone_properties
428
+
429
+ async def async_points_propriety(self, points_data) -> dict:
430
+ """Get the point propriety"""
431
+ point_properties = {}
432
+ id_count = 1
433
+ for point in points_data:
434
+ point_name = point.get("name")
435
+ coordinates = point.get("coordinates")
436
+ if coordinates and len(coordinates) > 0:
437
+ coordinates = point.get("coordinates")
438
+ x1, y1 = coordinates
439
+ point_properties[id_count] = {
440
+ "position": coordinates,
441
+ "name": point_name,
442
+ "x": x1,
443
+ "y": y1,
444
+ }
445
+ id_count += 1
446
+ if id_count > 1:
447
+ _LOGGER.debug("%s: Point Properties updated.", self.file_name)
448
+ return point_properties
@@ -70,7 +70,7 @@ class ImageDraw:
70
70
  self, img_np_array, pixels, layer_type, room_id, pixel_size, color_zone_clean
71
71
  ):
72
72
  """Process a room layer (segment or floor)."""
73
- room_color = self.img_h.rooms_colors[room_id]
73
+ room_color = self.img_h.shared.rooms_colors[room_id]
74
74
 
75
75
  try:
76
76
  if layer_type == "segment":
@@ -51,8 +51,6 @@ class HypferMapImageHandler(BaseHandler):
51
51
  self.offset_bottom = self.shared.offset_down # offset bottom
52
52
  self.offset_left = self.shared.offset_left # offset left
53
53
  self.offset_right = self.shared.offset_right # offset right
54
- self.offset_x = 0 # offset x for the aspect ratio.
55
- self.offset_y = 0 # offset y for the aspect ratio.
56
54
  self.imd = ImDraw(self)
57
55
  self.ac = AutoCrop(self)
58
56
  self.color_grey = (128, 128, 128, 255)
@@ -221,7 +219,7 @@ class HypferMapImageHandler(BaseHandler):
221
219
  )
222
220
  # Draw path prediction and paths.
223
221
  img_np_array = await self.imd.async_draw_paths(
224
- img_np_array, m_json, colors["color_move"], self.color_grey
222
+ img_np_array, m_json, colors["move"], self.color_grey
225
223
  )
226
224
  # Check if the robot is docked.
227
225
  if self.shared.vacuum_state == "docked":
@@ -235,13 +233,13 @@ class HypferMapImageHandler(BaseHandler):
235
233
  x=robot_position[0],
236
234
  y=robot_position[1],
237
235
  angle=robot_position_angle,
238
- fill=colors["color_robot"],
236
+ fill=colors["robot"],
239
237
  robot_state=self.shared.vacuum_state,
240
238
  )
241
239
  # Resize the image
242
240
  img_np_array = await self.ac.async_auto_trim_and_zoom_image(
243
241
  img_np_array,
244
- colors["color_background"],
242
+ colors["background"],
245
243
  int(self.shared.margins),
246
244
  int(self.shared.image_rotate),
247
245
  self.zooming,
@@ -303,7 +301,7 @@ class HypferMapImageHandler(BaseHandler):
303
301
  # Define the map points (fixed)
304
302
  map_points = self.get_map_points()
305
303
  # Calculate the calibration points in the vacuum coordinate system
306
- vacuum_points = self.imu.get_vacuum_points(rotation_angle)
304
+ vacuum_points = self.get_vacuum_points(rotation_angle)
307
305
 
308
306
  # Create the calibration data for each point
309
307
  for vacuum_point, map_point in zip(vacuum_points, map_points):
@@ -57,8 +57,6 @@ class ReImageHandler(BaseHandler):
57
57
  self.trim_up = None # Trim up
58
58
  self.zooming = False # Zooming flag
59
59
  self.file_name = self.shared.file_name # File name
60
- self.offset_x = 0 # offset x for the aspect ratio.
61
- self.offset_y = 0 # offset y for the aspect ratio.
62
60
  self.offset_top = self.shared.offset_top # offset top
63
61
  self.offset_bottom = self.shared.offset_down # offset bottom
64
62
  self.offset_left = self.shared.offset_left # offset left
@@ -121,9 +119,9 @@ class ReImageHandler(BaseHandler):
121
119
  "y": (y_min + y_max) // 2,
122
120
  }
123
121
  # get the zones and points data
124
- zone_properties = await self.imu.async_zone_propriety(zones_data)
122
+ zone_properties = await self.async_zone_propriety(zones_data)
125
123
  # get the points data
126
- point_properties = await self.imu.async_points_propriety(points_data)
124
+ point_properties = await self.async_points_propriety(points_data)
127
125
  if room_properties or zone_properties:
128
126
  extracted_data = [
129
127
  f"{len(room_properties)} Rooms" if room_properties else None,
@@ -381,7 +379,7 @@ class ReImageHandler(BaseHandler):
381
379
  map_points = self.get_map_points()
382
380
 
383
381
  # Valetudo Re version need corrections of the coordinates and are implemented with *10
384
- vacuum_points = self.imu.re_get_vacuum_points(rotation_angle)
382
+ vacuum_points = self.re_get_vacuum_points(rotation_angle)
385
383
 
386
384
  # Create the calibration data for each point
387
385
  for vacuum_point, map_point in zip(vacuum_points, map_points):
@@ -6,8 +6,6 @@ Version: 2024.12.0
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import hashlib
10
- import json
11
9
  import logging
12
10
 
13
11
  from .config.drawable import Drawable
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9.b5"
3
+ version = "0.1.9.b7"
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"
@@ -1,132 +0,0 @@
1
- """Utility code for the valetudo map parser."""
2
-
3
- import hashlib
4
- import json
5
- from logging import getLogger
6
-
7
- from PIL import ImageOps
8
-
9
- from ..images_utils import ImageUtils as ImUtils
10
- from .types import ChargerPosition, ImageSize, NumpyArray, RobotPosition
11
-
12
- _LOGGER = getLogger(__name__)
13
-
14
-
15
- class BaseHandler:
16
- """Avoid Code duplication"""
17
-
18
- def __init__(self):
19
- self.file_name = None
20
- self.img_size = None
21
- self.json_data = None
22
- self.json_id = None
23
- self.path_pixels = None
24
- self.robot_in_room = None
25
- self.robot_pos = None
26
- self.room_propriety = None
27
- self.rooms_pos = None
28
- self.charger_pos = None
29
- self.frame_number = 0
30
- self.max_frames = 1024
31
- self.crop_img_size = [0, 0]
32
- self.imu = ImUtils(self) # Image Utils
33
-
34
- def get_frame_number(self) -> int:
35
- """Return the frame number of the image."""
36
- return self.frame_number
37
-
38
- def get_robot_position(self) -> RobotPosition | None:
39
- """Return the robot position."""
40
- return self.robot_pos
41
-
42
- def get_charger_position(self) -> ChargerPosition | None:
43
- """Return the charger position."""
44
- return self.charger_pos
45
-
46
- def get_img_size(self) -> ImageSize | None:
47
- """Return the size of the image."""
48
- return self.img_size
49
-
50
- def get_json_id(self) -> str | None:
51
- """Return the JSON ID from the image."""
52
- return self.json_id
53
-
54
- async def async_resize_image(
55
- self, pil_img, width, height, aspect_ratio=None, is_rand=False
56
- ):
57
- """Resize the image to the given dimensions and aspect ratio."""
58
- if aspect_ratio:
59
- wsf, hsf = [int(x) for x in aspect_ratio.split(",")]
60
- if wsf == 0 or hsf == 0:
61
- return pil_img
62
- new_aspect_ratio = wsf / hsf
63
- if width / height > new_aspect_ratio:
64
- new_width = int(pil_img.height * new_aspect_ratio)
65
- new_height = pil_img.height
66
- else:
67
- new_width = pil_img.width
68
- new_height = int(pil_img.width / new_aspect_ratio)
69
- _LOGGER.debug(
70
- "%s: Image Aspect Ratio: %s, %s",
71
- self.file_name,
72
- str(wsf),
73
- str(hsf),
74
- )
75
- (
76
- self.crop_img_size[0],
77
- self.crop_img_size[1],
78
- ) = await self.async_map_coordinates_offset(
79
- wsf, hsf, new_width, new_height, is_rand
80
- )
81
- return ImageOps.pad(pil_img, (new_width, new_height))
82
- return ImageOps.pad(pil_img, (width, height))
83
-
84
- async def async_map_coordinates_offset(
85
- self, wsf: int, hsf: int, width: int, height: int, rand256: bool = False
86
- ) -> tuple[int, int]:
87
- """
88
- Offset the coordinates to the map.
89
- """
90
-
91
- if wsf == 1 and hsf == 1:
92
- self.imu.set_image_offset_ratio_1_1(width, height, rand256)
93
- elif wsf == 2 and hsf == 1:
94
- self.imu.set_image_offset_ratio_2_1(width, height, rand256)
95
- elif wsf == 3 and hsf == 2:
96
- self.imu.set_image_offset_ratio_3_2(width, height, rand256)
97
- elif wsf == 5 and hsf == 4:
98
- self.imu.set_image_offset_ratio_5_4(width, height, rand256)
99
- elif wsf == 9 and hsf == 16:
100
- self.imu.set_image_offset_ratio_9_16(width, height, rand256=True)
101
- elif wsf == 16 and hsf == 9:
102
- self.imu.set_image_offset_ratio_16_9(width, height, rand256=True)
103
- return width, height
104
-
105
- @staticmethod
106
- async def calculate_array_hash(layers: dict, active: list[int] = None) -> str:
107
- """Calculate the hash of the image based on layers and active zones."""
108
- if layers and active:
109
- data_to_hash = {
110
- "layers": len(layers["wall"][0]),
111
- "active_segments": tuple(active),
112
- }
113
- data_json = json.dumps(data_to_hash, sort_keys=True)
114
- return hashlib.sha256(data_json.encode()).hexdigest()
115
- return None
116
-
117
- @staticmethod
118
- async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
119
- """Copy the array."""
120
- return NumpyArray.copy(original_array)
121
-
122
- def get_map_points(self) -> dict:
123
- """Return the map points."""
124
- return [
125
- {"x": 0, "y": 0}, # Top-left corner 0
126
- {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
127
- {
128
- "x": self.crop_img_size[0],
129
- "y": self.crop_img_size[1],
130
- }, # Bottom-right corner 2
131
- {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
132
- ]
@@ -1,335 +0,0 @@
1
- """
2
- Image Utils Class for Valetudo Hypfer Image Handling.
3
- This class is used to simplify the ImageHandler class.
4
- Version: 0.1.6
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import logging
10
-
11
- _LOGGER = logging.getLogger(__name__)
12
-
13
-
14
- class ImageUtils:
15
- """Image Utils Class for Valetudo Hypfer Image Handler.
16
- It is used to simplify the ImageHandler class."""
17
-
18
- def __init__(self, image_handler):
19
- self.img = image_handler
20
- self.file_name = self.img.shared.file_name
21
-
22
- def get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
23
- """Calculate the calibration points based on the rotation angle."""
24
-
25
- # get_calibration_data
26
- vacuum_points = [
27
- {
28
- "x": self.img.crop_area[0] + self.img.offset_x,
29
- "y": self.img.crop_area[1] + self.img.offset_y,
30
- }, # Top-left corner 0
31
- {
32
- "x": self.img.crop_area[2] - self.img.offset_x,
33
- "y": self.img.crop_area[1] + self.img.offset_y,
34
- }, # Top-right corner 1
35
- {
36
- "x": self.img.crop_area[2] - self.img.offset_x,
37
- "y": self.img.crop_area[3] - self.img.offset_y,
38
- }, # Bottom-right corner 2
39
- {
40
- "x": self.img.crop_area[0] + self.img.offset_x,
41
- "y": self.img.crop_area[3] - self.img.offset_y,
42
- }, # Bottom-left corner (optional)3
43
- ]
44
-
45
- # Rotate the vacuum points based on the rotation angle
46
- if rotation_angle == 90:
47
- vacuum_points = [
48
- vacuum_points[1],
49
- vacuum_points[2],
50
- vacuum_points[3],
51
- vacuum_points[0],
52
- ]
53
- elif rotation_angle == 180:
54
- vacuum_points = [
55
- vacuum_points[2],
56
- vacuum_points[3],
57
- vacuum_points[0],
58
- vacuum_points[1],
59
- ]
60
- elif rotation_angle == 270:
61
- vacuum_points = [
62
- vacuum_points[3],
63
- vacuum_points[0],
64
- vacuum_points[1],
65
- vacuum_points[2],
66
- ]
67
-
68
- return vacuum_points
69
-
70
- def re_get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
71
- """Recalculate the calibration points based on the rotation angle.
72
- RAND256 Vacuums Calibration Points are in 10th of a mm."""
73
- vacuum_points = [
74
- {
75
- "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
76
- "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
77
- }, # Top-left corner 0
78
- {
79
- "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
80
- "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
81
- }, # Top-right corner 1
82
- {
83
- "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
84
- "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
85
- }, # Bottom-right corner 2
86
- {
87
- "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
88
- "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
89
- }, # Bottom-left corner (optional)3
90
- ]
91
-
92
- # Rotate the vacuum points based on the rotation angle
93
- if rotation_angle == 90:
94
- vacuum_points = [
95
- vacuum_points[1],
96
- vacuum_points[2],
97
- vacuum_points[3],
98
- vacuum_points[0],
99
- ]
100
- elif rotation_angle == 180:
101
- vacuum_points = [
102
- vacuum_points[2],
103
- vacuum_points[3],
104
- vacuum_points[0],
105
- vacuum_points[1],
106
- ]
107
- elif rotation_angle == 270:
108
- vacuum_points = [
109
- vacuum_points[3],
110
- vacuum_points[0],
111
- vacuum_points[1],
112
- vacuum_points[2],
113
- ]
114
-
115
- return vacuum_points
116
-
117
- def set_image_offset_ratio_1_1(
118
- self, width: int, height: int, rand256: bool = False
119
- ) -> None:
120
- """Set the image offset ratio to 1:1."""
121
-
122
- rotation = self.img.shared.image_rotate
123
- if not rand256:
124
- if rotation in [0, 180]:
125
- self.img.offset_y = self.img.crop_img_size[0] - width
126
- self.img.offset_x = (height - self.img.crop_img_size[1]) // 2
127
- elif rotation in [90, 270]:
128
- self.img.offset_y = width - self.img.crop_img_size[0]
129
- self.img.offset_x = (self.img.crop_img_size[1] - height) // 2
130
- else:
131
- if rotation in [0, 180]:
132
- self.img.offset_x = (width - self.img.crop_img_size[0]) // 2
133
- self.img.offset_y = height - self.img.crop_img_size[1]
134
- elif rotation in [90, 270]:
135
- self.img.offset_y = (self.img.crop_img_size[0] - width) // 2
136
- self.img.offset_x = self.img.crop_img_size[1] - height
137
- _LOGGER.debug(
138
- "%s Image Coordinates Offsets (x,y): %s. %s",
139
- self.file_name,
140
- self.img.offset_x,
141
- self.img.offset_y,
142
- )
143
-
144
- def set_image_offset_ratio_2_1(
145
- self, width: int, height: int, rand256: bool = False
146
- ) -> None:
147
- """Set the image offset ratio to 2:1."""
148
-
149
- rotation = self.img.shared.image_rotate
150
- if not rand256:
151
- if rotation in [0, 180]:
152
- self.img.offset_y = width - self.img.crop_img_size[0]
153
- self.img.offset_x = height - self.img.crop_img_size[1]
154
- elif rotation in [90, 270]:
155
- self.img.offset_x = width - self.img.crop_img_size[0]
156
- self.img.offset_y = height - self.img.crop_img_size[1]
157
- else:
158
- if rotation in [0, 180]:
159
- self.img.offset_y = width - self.img.crop_img_size[0]
160
- self.img.offset_x = height - self.img.crop_img_size[1]
161
- elif rotation in [90, 270]:
162
- self.img.offset_x = width - self.img.crop_img_size[0]
163
- self.img.offset_y = height - self.img.crop_img_size[1]
164
-
165
- _LOGGER.debug(
166
- "%s Image Coordinates Offsets (x,y): %s. %s",
167
- self.file_name,
168
- self.img.offset_x,
169
- self.img.offset_y,
170
- )
171
-
172
- def set_image_offset_ratio_3_2(
173
- self, width: int, height: int, rand256: bool = False
174
- ) -> None:
175
- """Set the image offset ratio to 3:2."""
176
-
177
- rotation = self.img.shared.image_rotate
178
-
179
- if not rand256:
180
- if rotation in [0, 180]:
181
- self.img.offset_y = width - self.img.crop_img_size[0]
182
- self.img.offset_x = ((height - self.img.crop_img_size[1]) // 2) - (
183
- self.img.crop_img_size[1] // 10
184
- )
185
- elif rotation in [90, 270]:
186
- self.img.offset_y = (self.img.crop_img_size[0] - width) // 2
187
- self.img.offset_x = (self.img.crop_img_size[1] - height) + (
188
- (height // 10) // 2
189
- )
190
- else:
191
- if rotation in [0, 180]:
192
- self.img.offset_x = (width - self.img.crop_img_size[0]) // 2
193
- self.img.offset_y = height - self.img.crop_img_size[1]
194
- elif rotation in [90, 270]:
195
- self.img.offset_y = (self.img.crop_img_size[0] - width) // 2
196
- self.img.offset_x = self.img.crop_img_size[1] - height
197
-
198
- _LOGGER.debug(
199
- "%s Image Coordinates Offsets (x,y): %s. %s",
200
- self.file_name,
201
- self.img.offset_x,
202
- self.img.offset_y,
203
- )
204
-
205
- def set_image_offset_ratio_5_4(
206
- self, width: int, height: int, rand256: bool = False
207
- ) -> None:
208
- """Set the image offset ratio to 5:4."""
209
-
210
- rotation = self.img.shared.image_rotate
211
- if not rand256:
212
- if rotation in [0, 180]:
213
- self.img.offset_x = ((width - self.img.crop_img_size[0]) // 2) - (
214
- self.img.crop_img_size[0] // 2
215
- )
216
- self.img.offset_y = (self.img.crop_img_size[1] - height) - (
217
- self.img.crop_img_size[1] // 2
218
- )
219
- elif rotation in [90, 270]:
220
- self.img.offset_y = ((self.img.crop_img_size[0] - width) // 2) - 10
221
- self.img.offset_x = (self.img.crop_img_size[1] - height) + (
222
- height // 10
223
- )
224
- else:
225
- if rotation in [0, 180]:
226
- self.img.offset_y = (width - self.img.crop_img_size[0]) // 2
227
- self.img.offset_x = self.img.crop_img_size[1] - height
228
- elif rotation in [90, 270]:
229
- self.img.offset_y = (self.img.crop_img_size[0] - width) // 2
230
- self.img.offset_x = self.img.crop_img_size[1] - height
231
-
232
- _LOGGER.debug(
233
- "%s Image Coordinates Offsets (x,y): %s. %s",
234
- self.file_name,
235
- self.img.offset_x,
236
- self.img.offset_y,
237
- )
238
-
239
- def set_image_offset_ratio_9_16(
240
- self, width: int, height: int, rand256: bool = False
241
- ) -> None:
242
- """Set the image offset ratio to 9:16."""
243
-
244
- rotation = self.img.shared.image_rotate
245
- if not rand256:
246
- if rotation in [0, 180]:
247
- self.img.offset_y = width - self.img.crop_img_size[0]
248
- self.img.offset_x = height - self.img.crop_img_size[1]
249
- elif rotation in [90, 270]:
250
- self.img.offset_x = (width - self.img.crop_img_size[0]) + (height // 10)
251
- self.img.offset_y = height - self.img.crop_img_size[1]
252
- else:
253
- if rotation in [0, 180]:
254
- self.img.offset_y = width - self.img.crop_img_size[0]
255
- self.img.offset_x = height - self.img.crop_img_size[1]
256
- elif rotation in [90, 270]:
257
- self.img.offset_x = width - self.img.crop_img_size[0]
258
- self.img.offset_y = height - self.img.crop_img_size[1]
259
-
260
- _LOGGER.debug(
261
- "%s Image Coordinates Offsets (x,y): %s. %s",
262
- self.file_name,
263
- self.img.offset_x,
264
- self.img.offset_y,
265
- )
266
-
267
- def set_image_offset_ratio_16_9(
268
- self, width: int, height: int, rand256: bool = False
269
- ) -> None:
270
- """Set the image offset ratio to 16:9."""
271
-
272
- rotation = self.img.shared.image_rotate
273
- if not rand256:
274
- if rotation in [0, 180]:
275
- self.img.offset_y = width - self.img.crop_img_size[0]
276
- self.img.offset_x = height - self.img.crop_img_size[1]
277
- elif rotation in [90, 270]:
278
- self.img.offset_x = width - self.img.crop_img_size[0]
279
- self.img.offset_y = height - self.img.crop_img_size[1]
280
- else:
281
- if rotation in [0, 180]:
282
- self.img.offset_y = width - self.img.crop_img_size[0]
283
- self.img.offset_x = height - self.img.crop_img_size[1]
284
- elif rotation in [90, 270]:
285
- self.img.offset_x = width - self.img.crop_img_size[0]
286
- self.img.offset_y = height - self.img.crop_img_size[1]
287
-
288
- _LOGGER.debug(
289
- "%s Image Coordinates Offsets (x,y): %s. %s",
290
- self.file_name,
291
- self.img.offset_x,
292
- self.img.offset_y,
293
- )
294
-
295
- async def async_zone_propriety(self, zones_data) -> dict:
296
- """Get the zone propriety"""
297
- zone_properties = {}
298
- id_count = 1
299
- for zone in zones_data:
300
- zone_name = zone.get("name")
301
- coordinates = zone.get("coordinates")
302
- if coordinates and len(coordinates) > 0:
303
- coordinates[0].pop()
304
- x1, y1, x2, y2 = coordinates[0]
305
- zone_properties[zone_name] = {
306
- "zones": coordinates,
307
- "name": zone_name,
308
- "x": ((x1 + x2) // 2),
309
- "y": ((y1 + y2) // 2),
310
- }
311
- id_count += 1
312
- if id_count > 1:
313
- _LOGGER.debug("%s: Zones Properties updated.", self.file_name)
314
- return zone_properties
315
-
316
- async def async_points_propriety(self, points_data) -> dict:
317
- """Get the point propriety"""
318
- point_properties = {}
319
- id_count = 1
320
- for point in points_data:
321
- point_name = point.get("name")
322
- coordinates = point.get("coordinates")
323
- if coordinates and len(coordinates) > 0:
324
- coordinates = point.get("coordinates")
325
- x1, y1 = coordinates
326
- point_properties[id_count] = {
327
- "position": coordinates,
328
- "name": point_name,
329
- "x": x1,
330
- "y": y1,
331
- }
332
- id_count += 1
333
- if id_count > 1:
334
- _LOGGER.debug("%s: Point Properties updated.", self.file_name)
335
- return point_properties